From dcc1158b797031d32317281826aa4be9a7bfa0da Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Thu, 25 Mar 2021 04:21:51 -0700 Subject: [PATCH 1/2] Trim TimeZoneInfo to reduce wasm size --- .../CMakeLists.txt | 6 ++- .../System.Private.CoreLib.Shared.projitems | 5 +-- .../src/System/TimeZoneInfo.Unix.cs | 41 +++++++++++++------ 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/CMakeLists.txt b/src/libraries/Native/Unix/System.Globalization.Native/CMakeLists.txt index c399629400f4aa..797ae3ceb2000d 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/CMakeLists.txt +++ b/src/libraries/Native/Unix/System.Globalization.Native/CMakeLists.txt @@ -61,10 +61,14 @@ set(NATIVEGLOBALIZATION_SOURCES pal_localeNumberData.c pal_localeStringData.c pal_normalization.c - pal_timeZoneInfo.c pal_icushim.c ) +# time zone names are filtered out of icu data for the browser and associated functionality is disabled +if (NOT CLR_CMAKE_TARGET_BROWSER) + set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} pal_timeZoneInfo.c) +endif() + if (NOT GEN_SHARED_LIB AND NOT CLR_CMAKE_TARGET_MACCATALYST AND NOT CLR_CMAKE_TARGET_IOS AND NOT CLR_CMAKE_TARGET_TVOS AND NOT CLR_CMAKE_TARGET_ANDROID AND NOT CLR_CMAKE_TARGET_BROWSER) set(NATIVEGLOBALIZATION_SOURCES ${NATIVEGLOBALIZATION_SOURCES} entrypoints.c) endif() diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 21e32141e93630..d4c05711bf2862 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1096,10 +1096,10 @@ Common\Interop\Interop.ResultCode.cs - + Common\Interop\Interop.TimeZoneDisplayNameType.cs - + Common\Interop\Interop.TimeZoneInfo.cs @@ -1909,7 +1909,6 @@ - diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 3a5899c6c2c243..1567f2e794c1b9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -22,9 +22,22 @@ public sealed partial class TimeZoneInfo private const string ZoneTabFileName = "zone.tab"; private const string TimeZoneEnvironmentVariable = "TZ"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; + +#if !TARGET_BROWSER private const string FallbackCultureName = "en-US"; private const string GmtId = "GMT"; + // Some time zones may give better display names using their location names rather than their generic name. + // We can update this list as need arises. + private static readonly string[] s_ZonesThatUseLocationName = new[] { + "Europe/Minsk", // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" + "Europe/Moscow", // Prefer "Moscow Time" over "Moscow Standard Time" + "Europe/Simferopol", // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" + "Pacific/Apia", // Prefer "Samoa Time" over "Apia Time" + "Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time" + }; +#endif + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when ICU is not available, // or when we get "GMT" returned from older ICU versions. (This list is not likely to change.) @@ -39,16 +52,6 @@ public sealed partial class TimeZoneInfo "Zulu" }; - // Some time zones may give better display names using their location names rather than their generic name. - // We can update this list as need arises. - private static readonly string[] s_ZonesThatUseLocationName = new[] { - "Europe/Minsk", // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" - "Europe/Moscow", // Prefer "Moscow Time" over "Moscow Standard Time" - "Europe/Simferopol", // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" - "Pacific/Apia", // Prefer "Samoa Time" over "Apia Time" - "Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time" - }; - private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { _id = id; @@ -119,6 +122,11 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; _displayName = _standardDisplayName; +#if TARGET_BROWSER + // For the browser, ICU time zone data is filtered out. The standard and daylight names will use the abbreviations set above, + // and the display name is composed of just the offset and IANA time zone ID (except UTC which is handled elsewhere). + _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; +#else // Determine the culture to use CultureInfo uiCulture = CultureInfo.CurrentUICulture; if (uiCulture.Name.Length == 0) @@ -128,6 +136,7 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref _standardDisplayName); GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref _daylightDisplayName); GetFullValueForDisplayNameField(_id, _baseUtcOffset, uiCulture, ref _displayName); +#endif // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns // with DateTimeOffset, SQL Server, and the W3C XML Specification @@ -145,7 +154,8 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); } - // Helper function that builds the value backing the DisplayName field from gloablization data. +#if !TARGET_BROWSER + // Helper function that builds the value backing the DisplayName field from globalization data. private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, CultureInfo uiCulture, ref string? displayName) { // There are a few diffent ways we might show the display name depending on the data. @@ -159,7 +169,6 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan // Try to get the generic name for this time zone. string? genericName = null; GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); - if (genericName == null) { // When we can't get a generic name, use the offset and the ID. @@ -271,6 +280,7 @@ private static string GetExemplarCityName(string timeZoneId, string uiCultureNam int i = timeZoneId.LastIndexOf('/'); return timeZoneId.Substring(i + 1).Replace('_', ' '); } +#endif // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. // However, there are some cases in the past where DST = true, and the daylight savings offset @@ -382,6 +392,7 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) private static unsafe string? GetAlternativeId(string id) { +#if !TARGET_BROWSER if (!GlobalizationMode.Invariant) { if (id.Equals("utc", StringComparison.OrdinalIgnoreCase)) @@ -406,7 +417,7 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) return new string(buffer, 0, length); } } - +#endif return null; } @@ -1965,6 +1976,9 @@ private static bool StringArrayContains(string value, string[] source, StringCom // Helper function to get the standard display name for the UTC static time zone instance private static string GetUtcStandardDisplayName() { +#if TARGET_BROWSER + return InvariantUtcStandardDisplayName; +#else // Don't bother looking up the name for invariant or English cultures CultureInfo uiCulture = CultureInfo.CurrentUICulture; if (GlobalizationMode.Invariant || uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en") @@ -1979,6 +1993,7 @@ private static string GetUtcStandardDisplayName() standardDisplayName = InvariantUtcStandardDisplayName; return standardDisplayName; +#endif } } } From 9241f70ad3f960e7758278cfbbf4feb31432553b Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Thu, 25 Mar 2021 22:49:28 -0700 Subject: [PATCH 2/2] Use partial class files instead of ifdefs --- .../System.Private.CoreLib.Shared.projitems | 3 +- .../TimeZoneInfo.FullGlobalizationData.cs | 259 ++++++++++++++++++ .../src/System/TimeZoneInfo.GetDisplayName.cs | 66 ----- .../TimeZoneInfo.MinimalGlobalizationData.cs | 25 ++ .../src/System/TimeZoneInfo.Unix.cs | 223 +-------------- 5 files changed, 293 insertions(+), 283 deletions(-) create mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs delete mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs create mode 100644 src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index d4c05711bf2862..998c13c94f4f95 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -1899,7 +1899,7 @@ - + @@ -1909,6 +1909,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs new file mode 100644 index 00000000000000..34e1f5adf6915e --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs @@ -0,0 +1,259 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; + +namespace System +{ + public sealed partial class TimeZoneInfo + { + private const string FallbackCultureName = "en-US"; + private const string GmtId = "GMT"; + + // Some time zones may give better display names using their location names rather than their generic name. + // We can update this list as need arises. + private static readonly string[] s_ZonesThatUseLocationName = new[] { + "Europe/Minsk", // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" + "Europe/Moscow", // Prefer "Moscow Time" over "Moscow Standard Time" + "Europe/Simferopol", // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" + "Pacific/Apia", // Prefer "Samoa Time" over "Apia Time" + "Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time" + }; + + // Main function that is called during construction to populate the three display names + private static void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName) + { + // Determine the culture to use + CultureInfo uiCulture = CultureInfo.CurrentUICulture; + if (uiCulture.Name.Length == 0) + uiCulture = CultureInfo.GetCultureInfo(FallbackCultureName); // ICU doesn't work nicely with InvariantCulture + + // Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data. + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName); + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref daylightDisplayName); + GetFullValueForDisplayNameField(timeZoneId, baseUtcOffset, uiCulture, ref displayName); + } + + // Helper function to get the standard display name for the UTC static time zone instance + private static string GetUtcStandardDisplayName() + { + // Don't bother looking up the name for invariant or English cultures + CultureInfo uiCulture = CultureInfo.CurrentUICulture; + if (GlobalizationMode.Invariant || uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en") + return InvariantUtcStandardDisplayName; + + // Try to get a localized version of "Coordinated Universal Time" from the globalization data + string? standardDisplayName = null; + GetDisplayName(UtcId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName); + + // Final safety check. Don't allow null or abbreviations + if (standardDisplayName == null || standardDisplayName == "GMT" || standardDisplayName == "UTC") + standardDisplayName = InvariantUtcStandardDisplayName; + + return standardDisplayName; + } + + // Helper function that retrieves various forms of time zone display names from ICU + private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName) + { + if (GlobalizationMode.Invariant) + { + return; + } + + string? timeZoneDisplayName; + bool result = Interop.CallStringMethod( + (buffer, locale, id, type) => + { + fixed (char* bufferPtr = buffer) + { + return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length); + } + }, + uiCulture, + timeZoneId, + nameType, + out timeZoneDisplayName); + + if (!result && uiCulture != FallbackCultureName) + { + // Try to fallback using FallbackCultureName just in case we can make it work. + result = Interop.CallStringMethod( + (buffer, locale, id, type) => + { + fixed (char* bufferPtr = buffer) + { + return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length); + } + }, + FallbackCultureName, + timeZoneId, + nameType, + out timeZoneDisplayName); + } + + // If there is an unknown error, don't set the displayName field. + // It will be set to the abbreviation that was read out of the tzfile. + if (result && !string.IsNullOrEmpty(timeZoneDisplayName)) + { + displayName = timeZoneDisplayName; + } + } + + // Helper function that builds the value backing the DisplayName field from globalization data. + private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, CultureInfo uiCulture, ref string? displayName) + { + // There are a few diffent ways we might show the display name depending on the data. + // The algorithm used below should avoid duplicating the same words while still achieving the + // goal of providing a unique, discoverable, and intuitive name. + + // Try to get the generic name for this time zone. + string? genericName = null; + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); + if (genericName == null) + { + // We'll use the fallback display name value already set. + return; + } + + // Get the base offset to prefix in front of the time zone. + // Only UTC and its aliases have "(UTC)", handled earlier. All other zones include an offset, even if it's zero. + string baseOffsetText = $"(UTC{(baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{baseUtcOffset:hh\\:mm})"; + + // Get the generic location name. + string? genericLocationName = null; + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref genericLocationName); + + // Some edge cases only apply when the offset is +00:00. + if (baseUtcOffset == TimeSpan.Zero) + { + // GMT and its aliases will just use the equivalent of "Greenwich Mean Time". + string? gmtLocationName = null; + GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref gmtLocationName); + if (genericLocationName == gmtLocationName) + { + displayName = $"{baseOffsetText} {genericName}"; + return; + } + + // Other zones with a zero offset and the equivalent of "Greenwich Mean Time" should only use the location name. + // For example, prefer "Iceland Time" over "Greenwich Mean Time (Reykjavik)". + string? gmtGenericName = null; + GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref gmtGenericName); + if (genericName == gmtGenericName) + { + displayName = $"{baseOffsetText} {genericLocationName}"; + return; + } + } + + if (genericLocationName == genericName) + { + // When the location name is the same as the generic name, + // then it is generally good enough to show by itself. + + // *** Example (en-US) *** + // id = "America/Havana" + // baseOffsetText = "(UTC-05:00)" + // standardName = "Cuba Standard Time" + // genericName = "Cuba Time" + // genericLocationName = "Cuba Time" + // exemplarCityName = "Havana" + // displayName = "(UTC-05:00) Cuba Time" + + displayName = $"{baseOffsetText} {genericLocationName}"; + return; + } + + // Prefer location names in some special cases. + if (StringArrayContains(timeZoneId, s_ZonesThatUseLocationName, StringComparison.OrdinalIgnoreCase)) + { + displayName = $"{baseOffsetText} {genericLocationName}"; + return; + } + + // See if we should include the exemplar city name. + string exemplarCityName = GetExemplarCityName(timeZoneId, uiCulture.Name); + if (uiCulture.CompareInfo.IndexOf(genericName, exemplarCityName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0 && genericLocationName != null) + { + // When an exemplar city is already part of the generic name, + // there's no need to repeat it again so just use the generic name. + + // *** Example (fr-FR) *** + // id = "Australia/Lord_Howe" + // baseOffsetText = "(UTC+10:30)" + // standardName = "heure normale de Lord Howe" + // genericName = "heure de Lord Howe" + // genericLocationName = "heure : Lord Howe" + // exemplarCityName = "Lord Howe" + // displayName = "(UTC+10:30) heure de Lord Howe" + + displayName = $"{baseOffsetText} {genericName}"; + } + else + { + // Finally, use the generic name and the exemplar city together. + // This provides an intuitive name and still disambiguates. + + // *** Example (en-US) *** + // id = "Europe/Rome" + // baseOffsetText = "(UTC+01:00)" + // standardName = "Central European Standard Time" + // genericName = "Central European Time" + // genericLocationName = "Italy Time" + // exemplarCityName = "Rome" + // displayName = "(UTC+01:00) Central European Time (Rome)" + + displayName = $"{baseOffsetText} {genericName} ({exemplarCityName})"; + } + } + + // Helper function that gets an exmplar city name either from ICU or from the IANA time zone ID itself + private static string GetExemplarCityName(string timeZoneId, string uiCultureName) + { + // First try to get the name through the localization data. + string? exemplarCityName = null; + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.ExemplarCity, uiCultureName, ref exemplarCityName); + if (!string.IsNullOrEmpty(exemplarCityName)) + return exemplarCityName; + + // Support for getting exemplar city names was added in ICU 51. + // We may have an older version. For example, in Helix we test on RHEL 7.5 which uses ICU 50.1.2. + // We'll fallback to using an English name generated from the time zone ID. + int i = timeZoneId.LastIndexOf('/'); + return timeZoneId.Substring(i + 1).Replace('_', ' '); + } + + // Helper function that returns an alternative ID using ICU data. Used primarily for converting from Windows IDs. + private static unsafe string? GetAlternativeId(string id) + { + if (!GlobalizationMode.Invariant) + { + if (id.Equals("utc", StringComparison.OrdinalIgnoreCase)) + { + // Special case UTC, as previously ICU would convert it to "Etc/GMT" which is incorrect name for UTC. + return "Etc/UTC"; + } + + foreach (char c in id) + { + // ICU uses some characters as a separator and trim the id at that character. + // while we should fail if the Id contained one of these characters. + if (c == '\\' || c == '\n' || c == '\r') + { + return null; + } + } + + char* buffer = stackalloc char[100]; + int length = Interop.Globalization.WindowsIdToIanaId(id, buffer, 100); + if (length > 0) + { + return new string(buffer, 0, length); + } + } + + return null; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs deleted file mode 100644 index ba54330cf89196..00000000000000 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Text; -using System.Threading; -using System.Security; - -using Internal.IO; - -namespace System -{ - public sealed partial class TimeZoneInfo - { - private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName) - { - if (GlobalizationMode.Invariant) - { - return; - } - - string? timeZoneDisplayName; - bool result = Interop.CallStringMethod( - (buffer, locale, id, type) => - { - fixed (char* bufferPtr = buffer) - { - return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length); - } - }, - uiCulture, - timeZoneId, - nameType, - out timeZoneDisplayName); - - if (!result && uiCulture != FallbackCultureName) - { - // Try to fallback using FallbackCultureName just in case we can make it work. - result = Interop.CallStringMethod( - (buffer, locale, id, type) => - { - fixed (char* bufferPtr = buffer) - { - return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length); - } - }, - FallbackCultureName, - timeZoneId, - nameType, - out timeZoneDisplayName); - } - - // If there is an unknown error, don't set the displayName field. - // It will be set to the abbreviation that was read out of the tzfile. - if (result && !string.IsNullOrEmpty(timeZoneDisplayName)) - { - displayName = timeZoneDisplayName; - } - } - } -} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs new file mode 100644 index 00000000000000..9da75786bed10f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System +{ + public sealed partial class TimeZoneInfo + { + private static void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName) + { + // Do nothing. We'll use the fallback values already set. + } + + private static string GetUtcStandardDisplayName() + { + // Just use the invariant display name. + return InvariantUtcStandardDisplayName; + } + + private static string? GetAlternativeId(string id) + { + // No alternative IDs in this target. + return null; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 1567f2e794c1b9..d2d79d0bf600b0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -23,24 +23,9 @@ public sealed partial class TimeZoneInfo private const string TimeZoneEnvironmentVariable = "TZ"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; -#if !TARGET_BROWSER - private const string FallbackCultureName = "en-US"; - private const string GmtId = "GMT"; - - // Some time zones may give better display names using their location names rather than their generic name. - // We can update this list as need arises. - private static readonly string[] s_ZonesThatUseLocationName = new[] { - "Europe/Minsk", // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" - "Europe/Moscow", // Prefer "Moscow Time" over "Moscow Standard Time" - "Europe/Simferopol", // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" - "Pacific/Apia", // Prefer "Samoa Time" over "Apia Time" - "Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time" - }; -#endif - // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml - // Hard-coded because we need to treat all aliases of UTC the same even when ICU is not available, - // or when we get "GMT" returned from older ICU versions. (This list is not likely to change.) + // Hard-coded because we need to treat all aliases of UTC the same even when globalization data is not available. + // (This list is not likely to change.) private static readonly string[] s_UtcAliases = new[] { "Etc/UTC", "Etc/UCT", @@ -117,26 +102,14 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) } } - // Use abbrev as the fallback + // Set fallback values using abbreviations, base offset, and id + // These are expected in environments without time zone globalization data _standardDisplayName = standardAbbrevName; _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; - _displayName = _standardDisplayName; - -#if TARGET_BROWSER - // For the browser, ICU time zone data is filtered out. The standard and daylight names will use the abbreviations set above, - // and the display name is composed of just the offset and IANA time zone ID (except UTC which is handled elsewhere). _displayName = $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"; -#else - // Determine the culture to use - CultureInfo uiCulture = CultureInfo.CurrentUICulture; - if (uiCulture.Name.Length == 0) - uiCulture = CultureInfo.GetCultureInfo(FallbackCultureName); // ICU doesn't work nicely with InvariantCulture - - // Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data. - GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref _standardDisplayName); - GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref _daylightDisplayName); - GetFullValueForDisplayNameField(_id, _baseUtcOffset, uiCulture, ref _displayName); -#endif + + // Try to populate the display names from the globalization data + TryPopulateTimeZoneDisplayNamesFromGlobalizationData(_id, _baseUtcOffset, ref _standardDisplayName, ref _daylightDisplayName, ref _displayName); // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns // with DateTimeOffset, SQL Server, and the W3C XML Specification @@ -154,134 +127,6 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); } -#if !TARGET_BROWSER - // Helper function that builds the value backing the DisplayName field from globalization data. - private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, CultureInfo uiCulture, ref string? displayName) - { - // There are a few diffent ways we might show the display name depending on the data. - // The algorithm used below should avoid duplicating the same words while still achieving the - // goal of providing a unique, discoverable, and intuitive name. - - // Get the base offset to prefix in front of the time zone. - // Only UTC and its aliases have "(UTC)", handled earlier. All other zones include an offset, even if it's zero. - string baseOffsetText = $"(UTC{(baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{baseUtcOffset:hh\\:mm})"; - - // Try to get the generic name for this time zone. - string? genericName = null; - GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); - if (genericName == null) - { - // When we can't get a generic name, use the offset and the ID. - // It is not ideal, but at least it is non-ambiguous. - // (Note, UTC was handled already above.) - displayName = $"{baseOffsetText} {timeZoneId}"; - return; - } - - // Get the generic location name. - string? genericLocationName = null; - GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref genericLocationName); - - // Some edge cases only apply when the offset is +00:00. - if (baseUtcOffset == TimeSpan.Zero) - { - // GMT and its aliases will just use the equivalent of "Greenwich Mean Time". - string? gmtLocationName = null; - GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref gmtLocationName); - if (genericLocationName == gmtLocationName) - { - displayName = $"{baseOffsetText} {genericName}"; - return; - } - - // Other zones with a zero offset and the equivalent of "Greenwich Mean Time" should only use the location name. - // For example, prefer "Iceland Time" over "Greenwich Mean Time (Reykjavik)". - string? gmtGenericName = null; - GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref gmtGenericName); - if (genericName == gmtGenericName) - { - displayName = $"{baseOffsetText} {genericLocationName}"; - return; - } - } - - if (genericLocationName == genericName) - { - // When the location name is the same as the generic name, - // then it is generally good enough to show by itself. - - // *** Example (en-US) *** - // id = "America/Havana" - // baseOffsetText = "(UTC-05:00)" - // standardName = "Cuba Standard Time" - // genericName = "Cuba Time" - // genericLocationName = "Cuba Time" - // exemplarCityName = "Havana" - // displayName = "(UTC-05:00) Cuba Time" - - displayName = $"{baseOffsetText} {genericLocationName}"; - return; - } - - // Prefer location names in some special cases. - if (StringArrayContains(timeZoneId, s_ZonesThatUseLocationName, StringComparison.OrdinalIgnoreCase)) - { - displayName = $"{baseOffsetText} {genericLocationName}"; - return; - } - - // See if we should include the exemplar city name. - string exemplarCityName = GetExemplarCityName(timeZoneId, uiCulture.Name); - if (uiCulture.CompareInfo.IndexOf(genericName, exemplarCityName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0 && genericLocationName != null) - { - // When an exemplar city is already part of the generic name, - // there's no need to repeat it again so just use the generic name. - - // *** Example (fr-FR) *** - // id = "Australia/Lord_Howe" - // baseOffsetText = "(UTC+10:30)" - // standardName = "heure normale de Lord Howe" - // genericName = "heure de Lord Howe" - // genericLocationName = "heure : Lord Howe" - // exemplarCityName = "Lord Howe" - // displayName = "(UTC+10:30) heure de Lord Howe" - - displayName = $"{baseOffsetText} {genericName}"; - } - else - { - // Finally, use the generic name and the exemplar city together. - // This provides an intuitive name and still disambiguates. - - // *** Example (en-US) *** - // id = "Europe/Rome" - // baseOffsetText = "(UTC+01:00)" - // standardName = "Central European Standard Time" - // genericName = "Central European Time" - // genericLocationName = "Italy Time" - // exemplarCityName = "Rome" - // displayName = "(UTC+01:00) Central European Time (Rome)" - - displayName = $"{baseOffsetText} {genericName} ({exemplarCityName})"; - } - } - - private static string GetExemplarCityName(string timeZoneId, string uiCultureName) - { - // First try to get the name through the localization data. - string? exemplarCityName = null; - GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.ExemplarCity, uiCultureName, ref exemplarCityName); - if (!string.IsNullOrEmpty(exemplarCityName)) - return exemplarCityName; - - // Support for getting exemplar city names was added in ICU 51. - // We may have an older version. For example, in Helix we test on RHEL 7.5 which uses ICU 50.1.2. - // We'll fallback to using an English name generated from the time zone ID. - int i = timeZoneId.LastIndexOf('/'); - return timeZoneId.Substring(i + 1).Replace('_', ' '); - } -#endif - // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. // However, there are some cases in the past where DST = true, and the daylight savings offset // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset @@ -390,37 +235,6 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) } } - private static unsafe string? GetAlternativeId(string id) - { -#if !TARGET_BROWSER - if (!GlobalizationMode.Invariant) - { - if (id.Equals("utc", StringComparison.OrdinalIgnoreCase)) - { - //special case UTC as ICU will convert it to "Etc/GMT" which is incorrect name for UTC. - return "Etc/UTC"; - } - foreach (char c in id) - { - // ICU uses some characters as a separator and trim the id at that character. - // while we should fail if the Id contained one of these characters. - if (c == '\\' || c == '\n' || c == '\r') - { - return null; - } - } - - char* buffer = stackalloc char[100]; - int length = Interop.Globalization.WindowsIdToIanaId(id, buffer, 100); - if (length > 0) - { - return new string(buffer, 0, length); - } - } -#endif - return null; - } - /// /// Helper function for retrieving the local system time zone. /// May throw COMException, TimeZoneNotFoundException, InvalidTimeZoneException. @@ -1972,28 +1786,5 @@ private static bool StringArrayContains(string value, string[] source, StringCom return false; } - - // Helper function to get the standard display name for the UTC static time zone instance - private static string GetUtcStandardDisplayName() - { -#if TARGET_BROWSER - return InvariantUtcStandardDisplayName; -#else - // Don't bother looking up the name for invariant or English cultures - CultureInfo uiCulture = CultureInfo.CurrentUICulture; - if (GlobalizationMode.Invariant || uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en") - return InvariantUtcStandardDisplayName; - - // Try to get a localized version of "Coordinated Universal Time" from the globalization data - string? standardDisplayName = null; - GetDisplayName(UtcId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName); - - // Final safety check. Don't allow null or abbreviations - if (standardDisplayName == null || standardDisplayName == "GMT" || standardDisplayName == "UTC") - standardDisplayName = InvariantUtcStandardDisplayName; - - return standardDisplayName; -#endif - } } }