ConvertIcuTimeFormatString: convert narrow no-break spaces to spaces too. (#83589)
authorTom Deseyn <tom.deseyn@gmail.com>
Tue, 4 Apr 2023 20:56:21 +0000 (22:56 +0200)
committerGitHub <noreply@github.com>
Tue, 4 Apr 2023 20:56:21 +0000 (13:56 -0700)
src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs
src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs
src/libraries/System.Runtime/tests/System/DateTimeTests.cs

index 09e3ed1dda3c26c4112f3393ecaa52b61e72a231..aa8cb4391d480eda24c0631d53ec4f7f7c842961 100644 (file)
@@ -232,13 +232,14 @@ namespace System.Globalization
 
             for (int i = 0; i < icuFormatString.Length; i++)
             {
-                switch (icuFormatString[i])
+                char current = icuFormatString[i];
+                switch (current)
                 {
                     case '\'':
                         result[resultPos++] = icuFormatString[i++];
                         while (i < icuFormatString.Length)
                         {
-                            char current = icuFormatString[i];
+                            current = icuFormatString[i];
                             result[resultPos++] = current;
                             if (current == '\'')
                             {
@@ -254,13 +255,10 @@ namespace System.Globalization
                     case 'h':
                     case 'm':
                     case 's':
-                        result[resultPos++] = icuFormatString[i];
-                        break;
-
                     case ' ':
-                    case '\u00A0':
-                        // Convert nonbreaking spaces into regular spaces
-                        result[resultPos++] = ' ';
+                    case '\u00A0': // no-break space
+                    case '\u202F': // narrow no-break space
+                        result[resultPos++] = current;
                         break;
 
                     case 'a': // AM/PM
index d8eac92c7b1325835f70cf19b3176ac0f2f6670e..ca1594b194eb3101e7ea67876369e7182a6eaa19 100644 (file)
@@ -5571,7 +5571,7 @@ new DS[] { DS.ERROR,  DS.TX_NNN,  DS.TX_NNN,  DS.TX_NNN,  DS.ERROR,   DS.ERROR,
                 // Check word by word
                 int targetPosition = 0;                 // Where we are in the target string
                 int thisPosition = Index;         // Where we are in this string
-                int wsIndex = target.AsSpan(targetPosition).IndexOfAny(' ', '\u00A0');
+                int wsIndex = target.AsSpan(targetPosition).IndexOfAny("\u0020\u00A0\u202F");
                 if (wsIndex < 0)
                 {
                     return false;
@@ -5615,7 +5615,7 @@ new DS[] { DS.ERROR,  DS.TX_NNN,  DS.TX_NNN,  DS.TX_NNN,  DS.ERROR,   DS.ERROR,
                         matchLength++;
                     }
 
-                    wsIndex = target.AsSpan(targetPosition).IndexOfAny(' ', '\u00A0');
+                    wsIndex = target.AsSpan(targetPosition).IndexOfAny("\u0020\u00A0\u202F");
                     if (wsIndex < 0)
                     {
                         break;
@@ -5678,7 +5678,8 @@ new DS[] { DS.ERROR,  DS.TX_NNN,  DS.TX_NNN,  DS.TX_NNN,  DS.ERROR,   DS.ERROR,
             {
                 return false;
             }
-            if (Value[Index] == ch)
+            if ((Value[Index] == ch) ||
+                (ch == ' ' && IsSpaceReplacingChar(Value[Index])))
             {
                 m_current = ch;
                 return true;
@@ -5687,6 +5688,8 @@ new DS[] { DS.ERROR,  DS.TX_NNN,  DS.TX_NNN,  DS.TX_NNN,  DS.ERROR,   DS.ERROR,
             return false;
         }
 
+        private static bool IsSpaceReplacingChar(char c) => c == '\u00a0' || c == '\u202f';
+
         //
         //  Actions: From the current position, try matching the longest word in the specified string array.
         //      E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
index 9373dfbca91f9e846ace4371833f9b1d3e063eb5..d83adc35b8dececc63e5aa76026275802a5ce954 100644 (file)
@@ -2008,6 +2008,57 @@ namespace System.Tests
             Assert.Equal(expected, DateTime.Parse(input, culture));
         }
 
+        public static IEnumerable<object[]> FormatAndParse_DifferentUnicodeSpaces_Succeeds_MemberData()
+        {
+            char[] spaceTypes = new[] { ' ',      // space
+                                        '\u00A0', // no-break space
+                                        '\u202F', // narrow no-break space
+                                      };
+            return spaceTypes.SelectMany(formatSpaceChar => spaceTypes.Select(parseSpaceChar => new object[] { formatSpaceChar, parseSpaceChar }));
+        }
+
+        [Theory]
+        [MemberData(nameof(FormatAndParse_DifferentUnicodeSpaces_Succeeds_MemberData))]
+        public void FormatAndParse_DifferentUnicodeSpaces_Succeeds(char formatSpaceChar, char parseSpaceChar)
+        {
+            var dateTime = new DateTime(2020, 5, 7, 9, 37, 40, DateTimeKind.Local);
+
+            DateTimeFormatInfo formatDtfi = CreateDateTimeFormatInfo(formatSpaceChar);
+            string formatted = dateTime.ToString(formatDtfi);
+            Assert.Contains(formatSpaceChar, formatted);
+
+            DateTimeFormatInfo parseDtfi = CreateDateTimeFormatInfo(parseSpaceChar);
+            Assert.Equal(dateTime, DateTime.Parse(formatted, parseDtfi));
+
+            static DateTimeFormatInfo CreateDateTimeFormatInfo(char spaceChar)
+            {
+                return new DateTimeFormatInfo()
+                {
+                    Calendar = DateTimeFormatInfo.InvariantInfo.Calendar,
+                    CalendarWeekRule = DateTimeFormatInfo.InvariantInfo.CalendarWeekRule,
+                    FirstDayOfWeek = DayOfWeek.Monday,
+                    AMDesignator = "AM",
+                    DateSeparator = "/",
+                    FullDateTimePattern = $"dddd,{spaceChar}MMMM{spaceChar}d,{spaceChar}yyyy{spaceChar}h:mm:ss{spaceChar}tt",
+                    LongDatePattern = $"dddd,{spaceChar}MMMM{spaceChar}d,{spaceChar}yyyy",
+                    LongTimePattern = $"h:mm:ss{spaceChar}tt",
+                    MonthDayPattern = "MMMM d",
+                    PMDesignator = "PM",
+                    ShortDatePattern = "M/d/yyyy",
+                    ShortTimePattern = $"h:mm{spaceChar}tt",
+                    TimeSeparator = ":",
+                    YearMonthPattern = $"MMMM{spaceChar}yyyy",
+                    AbbreviatedDayNames = new[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" },
+                    ShortestDayNames = new[] { "Su", "Mo", "Tu", "We", "Th", "Fr", "Sa" },
+                    DayNames = new[] { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" },
+                    AbbreviatedMonthNames = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" },
+                    MonthNames = new[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" },
+                    AbbreviatedMonthGenitiveNames = new[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "" },
+                    MonthGenitiveNames = new[] { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", "" }
+                };
+            }
+        }
+
         public static IEnumerable<object[]> ParseExact_ValidInput_Succeeds_MemberData()
         {
             foreach (DateTimeStyles style in new[] { DateTimeStyles.None, DateTimeStyles.AllowWhiteSpaces })