diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs index 09e3ed1dda3c2..aa8cb4391d480 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Icu.cs @@ -232,13 +232,14 @@ private static string ConvertIcuTimeFormatString(ReadOnlySpan icuFormatStr 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 @@ private static string ConvertIcuTimeFormatString(ReadOnlySpan icuFormatStr 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 diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs index d8eac92c7b132..ca1594b194eb3 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs @@ -5571,7 +5571,7 @@ internal bool MatchSpecifiedWords(string target, bool checkWordBoundary, scoped // 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 @@ internal bool MatchSpecifiedWords(string target, bool checkWordBoundary, scoped matchLength++; } - wsIndex = target.AsSpan(targetPosition).IndexOfAny(' ', '\u00A0'); + wsIndex = target.AsSpan(targetPosition).IndexOfAny("\u0020\u00A0\u202F"); if (wsIndex < 0) { break; @@ -5678,7 +5678,8 @@ internal bool Match(char ch) { return false; } - if (Value[Index] == ch) + if ((Value[Index] == ch) || + (ch == ' ' && IsSpaceReplacingChar(Value[Index]))) { m_current = ch; return true; @@ -5687,6 +5688,8 @@ internal bool Match(char ch) 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", diff --git a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs index 9373dfbca91f9..d83adc35b8dec 100644 --- a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs +++ b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs @@ -2008,6 +2008,57 @@ public static void Parse_ValidInput_Succeeds(string input, CultureInfo culture, Assert.Equal(expected, DateTime.Parse(input, culture)); } + public static IEnumerable 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 ParseExact_ValidInput_Succeeds_MemberData() { foreach (DateTimeStyles style in new[] { DateTimeStyles.None, DateTimeStyles.AllowWhiteSpaces })