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 7bfd0826ae183..cb38cba43e3b6 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 @@ -364,32 +364,59 @@ private static string ConvertIcuTimeFormatString(ReadOnlySpan icuFormatStr char current = icuFormatString[i]; switch (current) { + case '\\': + // The ICU format does not use backslashes to escape while the .NET format does + result[resultPos++] = '\\'; + result[resultPos++] = '\\'; + break; case '\'': - result[resultPos++] = icuFormatString[i++]; - while (i < icuFormatString.Length) + static bool HandleQuoteLiteral(ReadOnlySpan icuFormatString, ref int i, Span result, ref int resultPos) + { + if (i + 1 < icuFormatString.Length && icuFormatString[i + 1] == '\'') + { + result[resultPos++] = '\\'; + result[resultPos++] = '\''; + i++; + return true; + } + result[resultPos++] = '\''; + return false; + } + + if (HandleQuoteLiteral(icuFormatString, ref i, result, ref resultPos)) + { + break; + } + i++; + for (; i < icuFormatString.Length; i++) { current = icuFormatString[i]; - result[resultPos++] = current; if (current == '\'') { + if (HandleQuoteLiteral(icuFormatString, ref i, result, ref resultPos)) + { + continue; + } break; } - i++; + if (current == '\\') + { + // The same backslash escaping mentioned above + result[resultPos++] = '\\'; + } + result[resultPos++] = current; } break; - case ':': - case '.': case 'H': case 'h': case 'm': case 's': - case ' ': - case '\u00A0': // no-break space - case '\u202F': // narrow no-break space result[resultPos++] = current; break; case 'a': // AM/PM + case 'b': // am, pm, noon, midnight + case 'B': // flexible day periods if (!amPmAdded) { amPmAdded = true; @@ -398,6 +425,13 @@ private static string ConvertIcuTimeFormatString(ReadOnlySpan icuFormatStr } break; + default: + // Characters that are not ASCII letters are literal texts + if (!char.IsAsciiLetter(current)) + { + result[resultPos++] = current; + } + break; } } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoData.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoData.cs index 1a881675aa8d0..253d610184750 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoData.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoData.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Generic; using System.Runtime.InteropServices; using Xunit; @@ -58,5 +59,19 @@ public static Exception GetCultureNotSupportedException(CultureInfo cultureInfo) cultureInfo.Name, cultureInfo.Calendar.GetType().Name)); } + + // These cultures have bad ICU time patterns below the corresponding versions + // They are excluded from the VerifyTimePatterns tests + public static readonly Dictionary _badIcuTimePatterns = new Dictionary() + { + { "mi", new Version(65, 0) }, + { "mi-NZ", new Version(65, 0) }, + }; + public static bool HasBadIcuTimePatterns(CultureInfo culture) + { + return PlatformDetection.IsIcuGlobalizationAndNotHybridOnBrowser + && _badIcuTimePatterns.TryGetValue(culture.Name, out var version) + && PlatformDetection.ICUVersion < version; + } } } diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoLongTimePattern.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoLongTimePattern.cs index 920dbecb14daf..6cab9ab609772 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoLongTimePattern.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoLongTimePattern.cs @@ -284,6 +284,41 @@ public void LongTimePattern_CheckReadingTimeFormatWithSingleQuotes_ICU() } } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnBrowser))] + public void LongTimePattern_VerifyTimePatterns() + { + Assert.All(CultureInfo.GetCultures(CultureTypes.AllCultures), culture => { + if (DateTimeFormatInfoData.HasBadIcuTimePatterns(culture)) + { + return; + } + var pattern = culture.DateTimeFormat.LongTimePattern; + bool use24Hour = false; + bool use12Hour = false; + bool useAMPM = false; + for (var i = 0; i < pattern.Length; i++) + { + switch (pattern[i]) + { + case 'H': use24Hour = true; break; + case 'h': use12Hour = true; break; + case 't': useAMPM = true; break; + case '\\': i++; break; + case '\'': + i++; + for (; i < pattern.Length; i++) + { + var c = pattern[i]; + if (c == '\'') break; + if (c == '\\') i++; + } + break; + } + } + Assert.True((use24Hour || useAMPM) && (use12Hour ^ use24Hour), $"Bad long time pattern for culture {culture.Name}: '{pattern}'"); + }); + } + [Fact] public void LongTimePattern_CheckTimeFormatWithSpaces() { diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoShortTimePattern.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoShortTimePattern.cs index 5e54139c88fc0..31cd998fd18b7 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoShortTimePattern.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/DateTimeFormatInfo/DateTimeFormatInfoShortTimePattern.cs @@ -255,6 +255,41 @@ public void ShortTimePattern_SetReadOnly_ThrowsInvalidOperationException() Assert.Throws(() => DateTimeFormatInfo.InvariantInfo.ShortTimePattern = "HH:mm"); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnBrowser))] + public void ShortTimePattern_VerifyTimePatterns() + { + Assert.All(CultureInfo.GetCultures(CultureTypes.AllCultures), culture => { + if (DateTimeFormatInfoData.HasBadIcuTimePatterns(culture)) + { + return; + } + var pattern = culture.DateTimeFormat.ShortTimePattern; + bool use24Hour = false; + bool use12Hour = false; + bool useAMPM = false; + for (var i = 0; i < pattern.Length; i++) + { + switch (pattern[i]) + { + case 'H': use24Hour = true; break; + case 'h': use12Hour = true; break; + case 't': useAMPM = true; break; + case '\\': i++; break; + case '\'': + i++; + for (; i < pattern.Length; i++) + { + var c = pattern[i]; + if (c == '\'') break; + if (c == '\\') i++; + } + break; + } + } + Assert.True((use24Hour || useAMPM) && (use12Hour ^ use24Hour), $"Bad short time pattern for culture {culture.Name}: '{pattern}'"); + }); + } + [Fact] public void ShortTimePattern_CheckTimeFormatWithSpaces() {