From a7aabc47bdcee0d1f05e6ab2be6d2d95a813ee5c Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Thu, 1 Apr 2021 15:40:35 -0700 Subject: [PATCH] [WASM] Use consistent display names for UTC time zone --- .../TimeZoneInfo.FullGlobalizationData.cs | 7 +++ .../TimeZoneInfo.MinimalGlobalizationData.cs | 10 ++- .../src/System/TimeZoneInfo.Unix.cs | 2 +- .../src/System/TimeZoneInfo.Win32.cs | 7 +++ .../src/System/TimeZoneInfo.cs | 3 +- .../tests/System/TimeZoneInfoTests.cs | 62 +++++++++++++------ 6 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs index 34e1f5adf6915e..3e0230f9e81b2f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs @@ -7,6 +7,7 @@ namespace System { public sealed partial class TimeZoneInfo { + private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; private const string FallbackCultureName = "en-US"; private const string GmtId = "GMT"; @@ -53,6 +54,12 @@ private static string GetUtcStandardDisplayName() return standardDisplayName; } + // Helper function to get the full display name for the UTC static time zone instance + private static string GetUtcFullDisplayName(string timeZoneId, string standardDisplayName) + { + return $"(UTC) {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) { diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs index 9da75786bed10f..234d63366f3e13 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs @@ -12,8 +12,14 @@ private static void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string private static string GetUtcStandardDisplayName() { - // Just use the invariant display name. - return InvariantUtcStandardDisplayName; + // For this target, be consistent with other time zone display names that use an abbreviation. + return "UTC"; + } + + private static string GetUtcFullDisplayName(string timeZoneId, string standardDisplayName) + { + // For this target, be consistent with other time zone display names that use the ID. + return $"(UTC) {timeZoneId}"; } private static string? GetAlternativeId(string id) 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 3bd7e2f569b20f..6c7efd4b662b3f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -46,7 +46,7 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { _standardDisplayName = GetUtcStandardDisplayName(); _daylightDisplayName = _standardDisplayName; - _displayName = $"(UTC) {_standardDisplayName}"; + _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); _baseUtcOffset = TimeSpan.Zero; _adjustmentRules = Array.Empty(); return; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs index 8cc7ee8bb88f46..a13f48d6258de2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -34,6 +34,7 @@ public sealed partial class TimeZoneInfo private const string LastEntryValue = "LastEntry"; private const int MaxKeyLength = 255; + private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; private sealed partial class CachedData { @@ -1038,5 +1039,11 @@ private static string GetUtcStandardDisplayName() return standardDisplayName; } + + // Helper function to get the full display name for the UTC static time zone instance + private static string GetUtcFullDisplayName(string timeZoneId, string standardDisplayName) + { + return $"(UTC) {standardDisplayName}"; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 90a425e4f672f6..1ef3a1841ffd11 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -52,7 +52,6 @@ private enum TimeZoneInfoResult // constants for TimeZoneInfo.Local and TimeZoneInfo.Utc private const string UtcId = "UTC"; private const string LocalId = "Local"; - private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); @@ -2020,7 +2019,7 @@ private static bool IsValidAdjustmentRuleOffset(TimeSpan baseUtcOffset, Adjustme private static TimeZoneInfo CreateUtcTimeZone() { string standardDisplayName = GetUtcStandardDisplayName(); - string displayName = $"(UTC) {standardDisplayName}"; + string displayName = GetUtcFullDisplayName(UtcId, standardDisplayName); return CreateCustomTimeZone(UtcId, TimeSpan.Zero, displayName, standardDisplayName); } } diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index 0e8390632acf0d..ea673dea727b94 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -2335,14 +2335,55 @@ public static IEnumerable SystemTimeZonesTestData() private const string IanaAbbreviationPattern = @"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$"; private static readonly Regex s_IanaAbbreviationRegex = new Regex(IanaAbbreviationPattern); + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // (This list is not likely to change.) + private static readonly string[] s_UtcAliases = new[] { + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UCT", + "UTC", + "Universal", + "Zulu" + }; + [Theory] [MemberData(nameof(SystemTimeZonesTestData))] [PlatformSpecific(TestPlatforms.AnyUnix)] public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) { - if (timeZone.Id == TimeZoneInfo.Utc.Id || timeZone.StandardName == TimeZoneInfo.Utc.StandardName) + bool isUtc = s_UtcAliases.Contains(timeZone.Id, StringComparer.OrdinalIgnoreCase); + + if (PlatformDetection.IsBrowser) { - // UTC's display name is always the string "(UTC) " and the same text as the standard name. + // Browser platform doesn't have full ICU names, but uses the IANA IDs and abbreviations instead. + + // The display name will be the offset plus the ID. + // The offset is checked separately in TimeZoneInfo_DisplayNameStartsWithOffset + Assert.True(timeZone.DisplayName.EndsWith(" " + timeZone.Id), + $"Id: \"{timeZone.Id}\", DisplayName should have ended with the ID, Actual DisplayName: \"{timeZone.DisplayName}\""); + + if (isUtc) + { + // Make sure UTC and its aliases have exactly "UTC" for the standard and daylight names + Assert.True(timeZone.StandardName == "UTC", + $"Id: \"{timeZone.Id}\", Expected StandardName: \"UTC\", Actual StandardName: \"{timeZone.StandardName}\""); + Assert.True(timeZone.DaylightName == "UTC", + $"Id: \"{timeZone.Id}\", Expected DaylightName: \"UTC\", Actual DaylightName: \"{timeZone.DaylightName}\""); + } + else + { + // For other time zones, match any valid IANA time zone abbreviation, including numeric forms + Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.StandardName), + $"Id: \"{timeZone.Id}\", StandardName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual StandardName: \"{timeZone.StandardName}\""); + Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.DaylightName), + $"Id: \"{timeZone.Id}\", DaylightName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual DaylightName: \"{timeZone.DaylightName}\""); + } + } + else if (isUtc) + { + // UTC's display name is the string "(UTC) " and the same text as the standard name. Assert.True(timeZone.DisplayName == $"(UTC) {timeZone.StandardName}", $"Id: \"{timeZone.Id}\", Expected DisplayName: \"(UTC) {timeZone.StandardName}\", Actual DisplayName: \"{timeZone.DisplayName}\""); @@ -2354,21 +2395,6 @@ public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) Assert.True(timeZone.DaylightName == TimeZoneInfo.Utc.DaylightName, $"Id: \"{timeZone.Id}\", Expected DaylightName: \"{TimeZoneInfo.Utc.DaylightName}\", Actual DaylightName: \"{timeZone.DaylightName}\""); } - else if (PlatformDetection.IsBrowser) - { - // Browser platform doesn't have full ICU names, but uses the IANA data instead. - - // The display name will be the offset plus the ID. - // The offset is checked separately in TimeZoneInfo_DisplayNameStartsWithOffset - Assert.True(timeZone.DisplayName.EndsWith(" " + timeZone.Id), - $"Id: \"{timeZone.Id}\", DisplayName should have ended with the ID, Actual DisplayName: \"{timeZone.DisplayName}\""); - - // Match any valid IANA time zone abbreviation, including numeric forms - Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.StandardName), - $"Id: \"{timeZone.Id}\", StandardName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual StandardName: \"{timeZone.StandardName}\""); - Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.DaylightName), - $"Id: \"{timeZone.Id}\", DaylightName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual DaylightName: \"{timeZone.DaylightName}\""); - } else { // All we can really say generically here is that they aren't empty. @@ -2530,7 +2556,7 @@ public static void TimeZoneInfo_DaylightDeltaIsNoMoreThan12Hours() [MemberData(nameof(SystemTimeZonesTestData))] public static void TimeZoneInfo_DisplayNameStartsWithOffset(TimeZoneInfo tzi) { - if (tzi.StandardName == TimeZoneInfo.Utc.StandardName) + if (s_UtcAliases.Contains(tzi.Id, StringComparer.OrdinalIgnoreCase)) { // UTC and all of its aliases (Etc/UTC, and others) start with just "(UTC) " Assert.StartsWith("(UTC) ", tzi.DisplayName);