From b321f3b16bda3a016cf114d8355a5d072d615b48 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Fri, 17 Mar 2023 06:36:31 +0100 Subject: [PATCH 1/3] TimeFormat: icu: use no-break spaces while formatting, and support parsing them. --- .../src/System/Globalization/CultureData.Icu.cs | 14 ++++++-------- .../src/System/Globalization/DateTimeParse.cs | 9 ++++++--- 2 files changed, 12 insertions(+), 11 deletions(-) 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", From 31c335030a25a2cae1bc82333af6ad1135b98e88 Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Tue, 4 Apr 2023 13:14:52 +0200 Subject: [PATCH 2/3] Add test. --- .../tests/System/DateTimeTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs index 9373dfbca91f9..3f9a66c473454 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 timeSpaceChar) + { + return new DateTimeFormatInfo() + { + Calendar = DateTimeFormatInfo.InvariantInfo.Calendar, + CalendarWeekRule = DateTimeFormatInfo.InvariantInfo.CalendarWeekRule, + FirstDayOfWeek = DayOfWeek.Monday, + AMDesignator = "AM", + DateSeparator = "/", + FullDateTimePattern = $"dddd, MMMM d, yyyy h:mm:ss{timeSpaceChar}tt", + LongDatePattern = "dddd, MMMM d, yyyy", + LongTimePattern = $"h:mm:ss{timeSpaceChar}tt", + MonthDayPattern = "MMMM d", + PMDesignator = "PM", + ShortDatePattern = "M/d/yyyy", + ShortTimePattern = $"h:mm{timeSpaceChar}tt", + TimeSeparator = ":", + YearMonthPattern = "MMMM 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 }) From 3790cd2618cebcd87b59f69424ab1d53ff9e2fed Mon Sep 17 00:00:00 2001 From: Tom Deseyn Date: Tue, 4 Apr 2023 20:02:49 +0200 Subject: [PATCH 3/3] Replace all spaces in the format strings. --- .../System.Runtime/tests/System/DateTimeTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs index 3f9a66c473454..d83adc35b8dec 100644 --- a/src/libraries/System.Runtime/tests/System/DateTimeTests.cs +++ b/src/libraries/System.Runtime/tests/System/DateTimeTests.cs @@ -2030,7 +2030,7 @@ public void FormatAndParse_DifferentUnicodeSpaces_Succeeds(char formatSpaceChar, DateTimeFormatInfo parseDtfi = CreateDateTimeFormatInfo(parseSpaceChar); Assert.Equal(dateTime, DateTime.Parse(formatted, parseDtfi)); - static DateTimeFormatInfo CreateDateTimeFormatInfo(char timeSpaceChar) + static DateTimeFormatInfo CreateDateTimeFormatInfo(char spaceChar) { return new DateTimeFormatInfo() { @@ -2039,15 +2039,15 @@ static DateTimeFormatInfo CreateDateTimeFormatInfo(char timeSpaceChar) FirstDayOfWeek = DayOfWeek.Monday, AMDesignator = "AM", DateSeparator = "/", - FullDateTimePattern = $"dddd, MMMM d, yyyy h:mm:ss{timeSpaceChar}tt", - LongDatePattern = "dddd, MMMM d, yyyy", - LongTimePattern = $"h:mm:ss{timeSpaceChar}tt", + 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{timeSpaceChar}tt", + ShortTimePattern = $"h:mm{spaceChar}tt", TimeSeparator = ":", - YearMonthPattern = "MMMM yyyy", + 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" },