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 c6061ed1bd8528..7bfd0826ae183b 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 @@ -291,18 +291,34 @@ private unsafe string IcuGetTimeFormatString(bool shortFormat) Debug.Assert(!GlobalizationMode.UseNls); Debug.Assert(_sWindowsName != null, "[CultureData.GetTimeFormatString(bool shortFormat)] Expected _sWindowsName to be populated already"); - char* buffer = stackalloc char[ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY]; - - bool result = Interop.Globalization.GetLocaleTimeFormat(_sWindowsName, shortFormat, buffer, ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY); - if (!result) + ReadOnlySpan span; +#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS + if (GlobalizationMode.Hybrid) { - // Failed, just use empty string - Debug.Fail("[CultureData.GetTimeFormatString(bool shortFormat)] Failed"); - return string.Empty; + string res = Interop.Globalization.GetLocaleTimeFormatNative(_sWindowsName, shortFormat); + if (string.IsNullOrEmpty(res)) + { + Debug.Fail("[CultureData.GetTimeFormatString(bool shortFormat)] Failed"); + return string.Empty; + } + span = res.AsSpan(); + } + else +#endif + { + char* buffer = stackalloc char[ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY]; + bool result = Interop.Globalization.GetLocaleTimeFormat(_sWindowsName, shortFormat, buffer, ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY); + if (!result) + { + // Failed, just use empty string + Debug.Fail("[CultureData.GetTimeFormatString(bool shortFormat)] Failed"); + return string.Empty; + } + span = new ReadOnlySpan(buffer, ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY); + span = span.Slice(0, span.IndexOf('\0')); } - var span = new ReadOnlySpan(buffer, ICU_ULOC_KEYWORD_AND_VALUES_CAPACITY); - return ConvertIcuTimeFormatString(span.Slice(0, span.IndexOf('\0'))); + return ConvertIcuTimeFormatString(span); } // no support to lookup by region name, other than the hard-coded list in CultureData @@ -373,7 +389,6 @@ private static string ConvertIcuTimeFormatString(ReadOnlySpan icuFormatStr case '\u202F': // narrow no-break space result[resultPos++] = current; break; - case 'a': // AM/PM if (!amPmAdded) { diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Unix.cs index 6291881ba96e09..2a253d5367be30 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.Unix.cs @@ -20,11 +20,7 @@ internal sealed partial class CultureData private string[]? GetTimeFormatsCore(bool shortFormat) { -#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - string format = GlobalizationMode.Hybrid ? GetTimeFormatStringNative(shortFormat) : IcuGetTimeFormatString(shortFormat); -#else string format = IcuGetTimeFormatString(shortFormat); -#endif return new string[] { format }; } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index aff4cd0e13f26f..ed28dd5147a927 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -1978,11 +1978,7 @@ internal string TimeSeparator } else { -#if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS - string? longTimeFormat = GlobalizationMode.Hybrid ? GetTimeFormatStringNative() : IcuGetTimeFormatString(); -#else string? longTimeFormat = ShouldUseUserOverrideNlsData ? NlsGetTimeFormatString() : IcuGetTimeFormatString(); -#endif if (string.IsNullOrEmpty(longTimeFormat)) { longTimeFormat = LongTimes[0]; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.iOS.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.iOS.cs index 0617855576a88b..85be8ee54bcddc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.iOS.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.iOS.cs @@ -7,8 +7,6 @@ namespace System.Globalization { internal sealed partial class CultureData { - private const int LOC_FULLNAME_CAPACITY = 157; // max size of locale name - internal static string GetLocaleNameNative(string localeName) { return Interop.Globalization.GetLocaleNameNative(localeName); @@ -64,71 +62,5 @@ private int[] GetLocaleInfoNative(LocaleGroupingData type) return new int[] { primaryGroupingSize, secondaryGroupingSize }; } - - private string GetTimeFormatStringNative() => GetTimeFormatStringNative(shortFormat: false); - - private string GetTimeFormatStringNative(bool shortFormat) - { - Debug.Assert(_sWindowsName != null, "[CultureData.GetTimeFormatStringNative(bool shortFormat)] Expected _sWindowsName to be populated already"); - - string result = Interop.Globalization.GetLocaleTimeFormatNative(_sWindowsName, shortFormat); - - return ConvertNativeTimeFormatString(result); - } - - private static string ConvertNativeTimeFormatString(string nativeFormatString) - { - Span result = stackalloc char[LOC_FULLNAME_CAPACITY]; - - bool amPmAdded = false; - int resultPos = 0; - - for (int i = 0; i < nativeFormatString.Length; i++) - { - switch (nativeFormatString[i]) - { - case '\'': - result[resultPos++] = nativeFormatString[i++]; - while (i < nativeFormatString.Length) - { - char current = nativeFormatString[i]; - result[resultPos++] = current; - if (current == '\'') - { - break; - } - i++; - } - break; - - case ':': - case '.': - case 'H': - case 'h': - case 'm': - case 's': - result[resultPos++] = nativeFormatString[i]; - break; - - case ' ': - case '\u00A0': - // Convert nonbreaking spaces into regular spaces - result[resultPos++] = ' '; - break; - - case 'a': // AM/PM - if (!amPmAdded) - { - amPmAdded = true; - result[resultPos++] = 't'; - result[resultPos++] = 't'; - } - break; - - } - } - - return result.Slice(0, resultPos).ToString(); - } } } 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 669590d0fda4ff..920dbecb14daf3 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 @@ -283,5 +283,19 @@ public void LongTimePattern_CheckReadingTimeFormatWithSingleQuotes_ICU() } } } + + [Fact] + public void LongTimePattern_CheckTimeFormatWithSpaces() + { + var date = DateTime.Today + TimeSpan.FromHours(15) + TimeSpan.FromMinutes(15); + var culture = new CultureInfo("en-US"); + string formattedDate = date.ToString("t", culture); + bool containsSpace = formattedDate.Contains(' '); + bool containsNoBreakSpace = formattedDate.Contains('\u00A0'); + bool containsNarrowNoBreakSpace = formattedDate.Contains('\u202F'); + + Assert.True(containsSpace || containsNoBreakSpace || containsNarrowNoBreakSpace, + $"Formatted date string '{formattedDate}' does not contain any of the specified spaces."); + } } } 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 1913eaf47021e8..5e54139c88fc06 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 @@ -254,5 +254,19 @@ public void ShortTimePattern_SetReadOnly_ThrowsInvalidOperationException() { Assert.Throws(() => DateTimeFormatInfo.InvariantInfo.ShortTimePattern = "HH:mm"); } + + [Fact] + public void ShortTimePattern_CheckTimeFormatWithSpaces() + { + var date = DateTime.Today + TimeSpan.FromHours(15) + TimeSpan.FromMinutes(15); + var culture = new CultureInfo("en-US"); + string formattedDate = date.ToString("t", culture); + bool containsSpace = formattedDate.Contains(' '); + bool containsNoBreakSpace = formattedDate.Contains('\u00A0'); + bool containsNarrowNoBreakSpace = formattedDate.Contains('\u202F'); + + Assert.True(containsSpace || containsNoBreakSpace || containsNarrowNoBreakSpace, + $"Formatted date string '{formattedDate}' does not contain any of the specified spaces."); + } } } diff --git a/src/mono/browser/runtime/hybrid-globalization/calendar.ts b/src/mono/browser/runtime/hybrid-globalization/calendar.ts index dccb97820f1ff4..2a0d3ce70cbc68 100644 --- a/src/mono/browser/runtime/hybrid-globalization/calendar.ts +++ b/src/mono/browser/runtime/hybrid-globalization/calendar.ts @@ -5,7 +5,7 @@ import { VoidPtrNull } from "../types/internal"; import { runtimeHelpers } from "./module-exports"; import { Int32Ptr, VoidPtr } from "../types/emscripten"; -import { INNER_SEPARATOR, OUTER_SEPARATOR, normalizeSpaces } from "./helpers"; +import { INNER_SEPARATOR, OUTER_SEPARATOR } from "./helpers"; const MONTH_CODE = "MMMM"; const YEAR_CODE = "yyyy"; @@ -96,7 +96,6 @@ function getMonthYearPattern (locale: string | undefined, date: Date): string { pattern = pattern.replace("999", YEAR_CODE); // sometimes the number is localized and the above does not have an effect const yearStr = date.toLocaleDateString(locale, { year: "numeric" }); - pattern = normalizeSpaces(pattern); return pattern.replace(yearStr, YEAR_CODE); } @@ -165,7 +164,7 @@ function getShortDatePattern (locale: string | undefined): string { const localizedDayCode = dayStr.length == 1 ? "d" : "dd"; pattern = pattern.replace(dayStr, localizedDayCode); } - return normalizeSpaces(pattern); + return pattern; } function getLongDatePattern (locale: string | undefined, date: Date): string { @@ -196,7 +195,6 @@ function getLongDatePattern (locale: string | undefined, date: Date): string { pattern = pattern.replace(replacedWeekday, "dddd"); pattern = pattern.replace("22", DAY_CODE); const dayStr = date.toLocaleDateString(locale, { day: "numeric" }); // should we replace it for localized digits? - pattern = normalizeSpaces(pattern); return pattern.replace(dayStr, DAY_CODE); } diff --git a/src/mono/browser/runtime/hybrid-globalization/culture-info.ts b/src/mono/browser/runtime/hybrid-globalization/culture-info.ts index aa4e3f152d0d42..6c938198dabb74 100644 --- a/src/mono/browser/runtime/hybrid-globalization/culture-info.ts +++ b/src/mono/browser/runtime/hybrid-globalization/culture-info.ts @@ -4,7 +4,7 @@ import { VoidPtrNull } from "../types/internal"; import { runtimeHelpers } from "./module-exports"; import { Int32Ptr, VoidPtr } from "../types/emscripten"; -import { OUTER_SEPARATOR, normalizeLocale, normalizeSpaces } from "./helpers"; +import { OUTER_SEPARATOR, normalizeLocale } from "./helpers"; export function mono_wasm_get_culture_info (culture: number, cultureLength: number, dst: number, dstMaxLength: number, dstLength: Int32Ptr): VoidPtr { try { @@ -91,7 +91,7 @@ function getLongTimePattern (locale: string | undefined, designators: any): stri hourPattern = hasPrefix ? "hh" : "h"; pattern = pattern.replace(hasPrefix ? hour12WithPrefix : localizedHour12, hourPattern); } - return normalizeSpaces(pattern); + return pattern; } function getShortTimePattern (pattern: string): string { diff --git a/src/mono/browser/runtime/hybrid-globalization/helpers.ts b/src/mono/browser/runtime/hybrid-globalization/helpers.ts index 0cd2294447226f..5bf28026404a9c 100644 --- a/src/mono/browser/runtime/hybrid-globalization/helpers.ts +++ b/src/mono/browser/runtime/hybrid-globalization/helpers.ts @@ -26,15 +26,6 @@ export function normalizeLocale (locale: string | null) { } } -export function normalizeSpaces (pattern: string) { - if (!pattern.includes("\u202F")) - return pattern; - - // if U+202F present, replace them with spaces - return pattern.replace("\u202F", "\u0020"); -} - - export function isSurrogate (str: string, startIdx: number): boolean { return SURROGATE_HIGHER_START <= str[startIdx] && str[startIdx] <= SURROGATE_HIGHER_END &&