diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs index 2b381382072e7..b396927f60dc9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using System.Threading; +using System.Diagnostics; namespace System { @@ -21,24 +23,7 @@ public sealed partial class TimeZoneInfo "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) - { - if (GlobalizationMode.Invariant) - { - return; - } - - // 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); - } + private static CultureInfo? _uiCulture; // Helper function to get the standard display name for the UTC static time zone instance private static string GetUtcStandardDisplayName() @@ -67,6 +52,35 @@ private static string GetUtcFullDisplayName(string timeZoneId, string standardDi } #pragma warning restore IDE0060 + private static CultureInfo UICulture + { + get + { + if (_uiCulture == null) + { + Debug.Assert(!GlobalizationMode.Invariant); + // 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 + + Interlocked.CompareExchange(ref _uiCulture, uiCulture, null); + } + + return _uiCulture; + } + } + + private static void GetStandardDisplayName(string timeZoneId, ref string? displayName) + { + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Standard, UICulture.Name, ref displayName); + } + + private static void GetDaylightDisplayName(string timeZoneId, ref string? displayName) + { + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, UICulture.Name, ref displayName); + } + // 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) { @@ -115,7 +129,7 @@ private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalizati } // 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) + private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, 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 @@ -123,6 +137,7 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan // Try to get the generic name for this time zone. string? genericName = null; + CultureInfo uiCulture = UICulture; GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); if (genericName == null) { 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 d04de84dcc8f8..fa1d25e493335 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs @@ -6,7 +6,11 @@ namespace System public sealed partial class TimeZoneInfo { #pragma warning disable IDE0060 - static partial void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName); + static partial void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, ref string? displayName); + + static partial void GetStandardDisplayName(string timeZoneId, ref string? displayName); + + static partial void GetDaylightDisplayName(string timeZoneId, ref string? displayName); private static string GetUtcStandardDisplayName() { 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 87a03dcdfa218..992035b92df60 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -18,19 +18,36 @@ public sealed partial class TimeZoneInfo { private const string DefaultTimeZoneDirectory = "/usr/share/zoneinfo/"; - // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // Set fallback values using abbreviations, base offset, and id + // These are expected in environments without time zone globalization data + private string? _standardAbbrevName; + private string? _daylightAbbrevName; + + // Handle UTC and its 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 globalization data is not available. // (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" - }; + private static bool IsUtcAlias (string id) + { + switch ((ushort)id[0]) + { + case 69: // e + case 101: // E + return string.Equals(id, "Etc/UTC", StringComparison.OrdinalIgnoreCase) || + string.Equals(id, "Etc/Universal", StringComparison.OrdinalIgnoreCase) || + string.Equals(id, "Etc/UTC", StringComparison.OrdinalIgnoreCase) || + string.Equals(id, "Etc/Zulu", StringComparison.OrdinalIgnoreCase); + case 85: // u + case 117: // U + return string.Equals(id, "UCT", StringComparison.OrdinalIgnoreCase) || + string.Equals(id, "UTC", StringComparison.OrdinalIgnoreCase) || + string.Equals(id, "Universal", StringComparison.OrdinalIgnoreCase); + case 90: // z + case 122: // Z + return string.Equals(id, "Zulu", StringComparison.OrdinalIgnoreCase); + } + + return false; + } private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { @@ -38,28 +55,21 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) HasIanaId = true; - // Handle UTC and its aliases - if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) + if (IsUtcAlias(id)) { - _standardDisplayName = GetUtcStandardDisplayName(); - _daylightDisplayName = _standardDisplayName; - _displayName = GetUtcFullDisplayName(_id, _standardDisplayName); _baseUtcOffset = TimeSpan.Zero; _adjustmentRules = Array.Empty(); return; } - TZifHead t; DateTime[] dts; byte[] typeOfLocalTime; TZifType[] transitionType; string zoneAbbreviations; string? futureTransitionsPosixFormat; - string? standardAbbrevName = null; - string? daylightAbbrevName = null; // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. - TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out futureTransitionsPosixFormat); + TZif_ParseRaw(data, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out futureTransitionsPosixFormat); // find the best matching baseUtcOffset and display strings based on the current utcNow value. // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later @@ -71,11 +81,11 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) if (!transitionType[type].IsDst) { _baseUtcOffset = transitionType[type].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + _standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); } else { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); + _daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[type].AbbreviationIndex); } } @@ -88,24 +98,15 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) if (!transitionType[i].IsDst) { _baseUtcOffset = transitionType[i].UtcOffset; - standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + _standardAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); } else { - daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); + _daylightAbbrevName = TZif_GetZoneAbbreviation(zoneAbbreviations, transitionType[i].AbbreviationIndex); } } } - // 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 = string.Create(null, stackalloc char[256], $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"); - - // 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 if (_baseUtcOffset.Ticks % TimeSpan.TicksPerMinute != 0) @@ -219,6 +220,50 @@ public AdjustmentRule[] GetAdjustmentRules() return rulesList.ToArray(); } + private string? PopulateDisplayName() + { + if (IsUtcAlias(Id)) + return GetUtcFullDisplayName(Id, StandardName); + + // Set fallback value using abbreviations, base offset, and id + // These are expected in environments without time zone globalization data + string? displayName = string.Create(null, stackalloc char[256], $"(UTC{(_baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{_baseUtcOffset:hh\\:mm}) {_id}"); + if (GlobalizationMode.Invariant) + return displayName; + + GetFullValueForDisplayNameField(Id, BaseUtcOffset, ref displayName); + + return displayName; + } + + private string? PopulateStandardDisplayName() + { + if (IsUtcAlias(Id)) + return GetUtcStandardDisplayName(); + + string? standardDisplayName = _standardAbbrevName; + if (GlobalizationMode.Invariant) + return standardDisplayName; + + GetStandardDisplayName(Id, ref standardDisplayName); + + return standardDisplayName; + } + + private string? PopulateDaylightDisplayName() + { + if (IsUtcAlias(Id)) + return StandardName; + + string? daylightDisplayName = _daylightAbbrevName ?? _standardAbbrevName; + if (GlobalizationMode.Invariant) + return daylightDisplayName; + + GetDaylightDisplayName(Id, ref daylightDisplayName); + + return daylightDisplayName; + } + private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); @@ -1065,7 +1110,7 @@ private static DateTime TZif_UnixTimeToDateTime(long unixTime) => unixTime > DateTimeOffset.UnixMaxSeconds ? DateTime.MaxValue : DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime; - private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, + private static void TZif_ParseRaw(byte[] data, out DateTime[] dts, out byte[] typeOfLocalTime, out TZifType[] transitionType, out string zoneAbbreviations, out string? futureTransitionsPosixFormat) { futureTransitionsPosixFormat = null; @@ -1073,7 +1118,7 @@ private static void TZif_ParseRaw(byte[] data, out TZifHead t, out DateTime[] dt // read in the 44-byte TZ header containing the count/length fields // int index = 0; - t = new TZifHead(data, index); + TZifHead t = new TZifHead(data, index); index += TZifHead.Length; int timeValuesLength = 4; // the first version uses 4-bytes to specify times 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 3344e9ef82b77..1bee4bd2127b0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -88,6 +88,30 @@ public AdjustmentRule[] GetAdjustmentRules() return (AdjustmentRule[])_adjustmentRules.Clone(); } + private static string? PopulateDisplayName() + { + // Keep window's implementation to populate via constructor + // This should not be reached + Debug.Assert(false); + return null; + } + + private static string? PopulateStandardDisplayName() + { + // Keep window's implementation to populate via constructor + // This should not be reached + Debug.Assert(false); + return null; + } + + private static string? PopulateDaylightDisplayName() + { + // Keep window's implementation to populate via constructor + // This should not be reached + Debug.Assert(false); + return null; + } + private static void PopulateAllSystemTimeZones(CachedData cachedData) { Debug.Assert(Monitor.IsEntered(cachedData)); @@ -900,9 +924,9 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out value = new TimeZoneInfo( id, new TimeSpan(0, -(defaultTimeZoneInformation.Bias), 0), - displayName, - standardName, - daylightName, + displayName ?? string.Empty, + standardName ?? string.Empty, + daylightName ?? string.Empty, adjustmentRules, disableDaylightSavingTime: false); diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 97c6d0a10f7ae..583212906e68c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -44,9 +44,9 @@ private enum TimeZoneInfoResult private const int MaxKeyLength = 255; private readonly string _id; - private readonly string? _displayName; - private readonly string? _standardDisplayName; - private readonly string? _daylightDisplayName; + private string? _displayName; + private string? _standardDisplayName; + private string? _daylightDisplayName; private readonly TimeSpan _baseUtcOffset; private readonly bool _supportsDaylightSavingTime; private readonly AdjustmentRule[]? _adjustmentRules; @@ -85,9 +85,9 @@ private TimeZoneInfo CreateLocal() timeZone = new TimeZoneInfo( timeZone._id, timeZone._baseUtcOffset, - timeZone._displayName, - timeZone._standardDisplayName, - timeZone._daylightDisplayName, + timeZone.DisplayName, + timeZone.StandardName, + timeZone.DaylightName, timeZone._adjustmentRules, disableDaylightSavingTime: false, timeZone.HasIanaId); @@ -146,11 +146,38 @@ public DateTimeKind GetCorrespondingKind(TimeZoneInfo? timeZone) /// public bool HasIanaId { get; } - public string DisplayName => _displayName ?? string.Empty; + public string DisplayName + { + get + { + if (_displayName == null) + Interlocked.CompareExchange(ref _displayName, PopulateDisplayName(), null); + + return _displayName ?? string.Empty; + } + } - public string StandardName => _standardDisplayName ?? string.Empty; + public string StandardName + { + get + { + if (_standardDisplayName == null) + Interlocked.CompareExchange(ref _standardDisplayName, PopulateStandardDisplayName(), null); - public string DaylightName => _daylightDisplayName ?? string.Empty; + return _standardDisplayName ?? string.Empty; + } + } + + public string DaylightName + { + get + { + if (_daylightDisplayName == null) + Interlocked.CompareExchange(ref _daylightDisplayName, PopulateDaylightDisplayName(), null); + + return _daylightDisplayName ?? string.Empty; + } + } public TimeSpan BaseUtcOffset => _baseUtcOffset; @@ -962,9 +989,9 @@ private TimeZoneInfo( _id = id; _baseUtcOffset = baseUtcOffset; - _displayName = displayName; - _standardDisplayName = standardDisplayName; - _daylightDisplayName = disableDaylightSavingTime ? null : daylightDisplayName; + _displayName = displayName ?? string.Empty; + _standardDisplayName = standardDisplayName ?? string.Empty; + _daylightDisplayName = disableDaylightSavingTime ? string.Empty : daylightDisplayName ?? string.Empty; _supportsDaylightSavingTime = adjustmentRulesSupportDst && !disableDaylightSavingTime; _adjustmentRules = adjustmentRules;