// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Globalization; using System.Threading; namespace System { public sealed partial class TimeZoneInfo { private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; private const string FallbackCultureName = "en-US"; #if !TARGET_MACCATALYST && !TARGET_IOS && !TARGET_TVOS 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 private static CultureInfo? _uiCulture; // Helper function to get the standard display name for the UTC static time zone instance private static string GetUtcStandardDisplayName() { #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (!GlobalizationMode.Hybrid) { // For this target, be consistent with other time zone display names that use an abbreviation. return "UTC"; } #endif // 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; } #pragma warning disable IDE0060 // Helper function to get the full display name for the UTC static time zone instance private static string GetUtcFullDisplayName(string timeZoneId, string standardDisplayName) { #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (!GlobalizationMode.Hybrid) { // For this target, be consistent with other time zone display names that use the ID. return $"(UTC) {timeZoneId}"; } #endif return $"(UTC) {standardDisplayName}"; } #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) { if (GlobalizationMode.Invariant) { return; } string? timeZoneDisplayName; bool result = Interop.CallStringMethod( (buffer, locale, id, type) => { fixed (char* bufferPtr = buffer) { #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) return Interop.Globalization.GetTimeZoneDisplayNameNative(locale, locale.Length, id, id.Length, type, bufferPtr, buffer.Length); #endif 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) { #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS if (GlobalizationMode.Hybrid) return Interop.Globalization.GetTimeZoneDisplayNameNative(locale, locale.Length, id, id.Length, type, bufferPtr, buffer.Length); #endif 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, ref string? displayName) { CultureInfo uiCulture = UICulture; // 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 = string.Create(null, stackalloc char[128], $"(UTC{(baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{baseUtcOffset:hh\\:mm})"); #if TARGET_MACCATALYST || TARGET_IOS || TARGET_TVOS Debug.Assert(GlobalizationMode.Hybrid); string? timeZoneName = null; GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.TimeZoneName, uiCulture.Name, ref timeZoneName); // For this target, be consistent with other time zone display names that use the ID. displayName = $"{baseOffsetText} {timeZoneName}"; return; #else // There are a few different 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 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})"; } #endif } // 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, out bool idIsIana) { idIsIana = false; return TryConvertWindowsIdToIanaId(id, null, out string? ianaId) ? ianaId : null; } } } |