diff --git a/src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs b/src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs index faa7e4f3a9073..10e73ee4b1d79 100644 --- a/src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs +++ b/src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs @@ -16,7 +16,7 @@ internal static extern unsafe ResultCode GetTimeZoneDisplayName( int resultLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_WindowsIdToIanaId")] - internal static extern unsafe int WindowsIdToIanaId(string windowsId, char* ianaId, int ianaIdLength); + internal static extern unsafe int WindowsIdToIanaId(string windowsId, [MarshalAs(UnmanagedType.LPStr)] string? region, char* ianaId, int ianaIdLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_IanaIdToWindowsId")] internal static extern unsafe int IanaIdToWindowsId(string ianaId, char* windowsId, int windowsIdLength); diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c index 9173931716c0e..2620c146b53a0 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c @@ -21,13 +21,13 @@ static const UChar EXEMPLAR_CITY_PATTERN_UCHAR[] = {'V', 'V', 'V', '\0'}; /* Convert Windows Time Zone Id to IANA Id */ -int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, UChar* ianaId, int32_t ianaIdLength) +int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength) { UErrorCode status = U_ZERO_ERROR; if (ucal_getTimeZoneIDForWindowsID_ptr != NULL) { - int32_t ianaIdFilledLength = ucal_getTimeZoneIDForWindowsID(windowsId, -1, NULL, ianaId, ianaIdLength, &status); + int32_t ianaIdFilledLength = ucal_getTimeZoneIDForWindowsID(windowsId, -1, region, ianaId, ianaIdLength, &status); if (U_SUCCESS(status)) { return ianaIdFilledLength; @@ -91,7 +91,7 @@ static void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* if (U_SUCCESS(*err)) { udat_format(dateFormatter, timestamp, result, resultLength, NULL, err); - udat_close(dateFormatter); + udat_close(dateFormatter); } } diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h index 49bfb6250eb50..b91469e6a1341 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h @@ -20,6 +20,6 @@ typedef enum TimeZoneDisplayName_ExemplarCity = 4, } TimeZoneDisplayNameType; -PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, UChar* ianaId, int32_t ianaIdLength); +PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength); PALEXPORT int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* windowsId, int32_t windowsIdLength); PALEXPORT ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength); 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 9256e41f8a633..2597164d6bbeb 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 @@ -1045,6 +1045,7 @@ + @@ -1902,7 +1903,7 @@ - + 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 new file mode 100644 index 0000000000000..d1f306abb0f6d --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.Unix.cs @@ -0,0 +1,241 @@ +// 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 InvariantUtcStandardDisplayName = "Coordinated Universal Time"; + 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 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) + { + 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, out bool idIsIana) + { + idIsIana = false; + return TryConvertWindowsIdToIanaId(id, null, out string? ianaId) ? ianaId : null; + } + } +} 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 3e0230f9e81b2..557e333d4eb9a 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.FullGlobalizationData.cs @@ -7,260 +7,75 @@ 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"; - - // 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 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) + private static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, bool allocate, out string? windowsId) { - if (GlobalizationMode.Invariant) + if (GlobalizationMode.Invariant || GlobalizationMode.UseNls || ianaId is null) { - return; + windowsId = null; + return false; } - 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) + foreach (char c in ianaId) { - // 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); + // 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') + { + windowsId = null; + return false; + } } - // 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)) + char* buffer = stackalloc char[100]; + int length = Interop.Globalization.IanaIdToWindowsId(ianaId, buffer, 100); + if (length > 0) { - displayName = timeZoneDisplayName; + windowsId = allocate ? new string(buffer, 0, length) : null; + return true; } + + windowsId = null; + return false; } - // 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 unsafe bool TryConvertWindowsIdToIanaId(string windowsId, string? region, bool allocate, out string? ianaId) { - // 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) + // This functionality is not enabled in the browser for the sake of size reduction. + if (GlobalizationMode.Invariant || GlobalizationMode.UseNls || windowsId is null) { - // We'll use the fallback display name value already set. - return; + ianaId = null; + return false; } - // 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) + if (windowsId.Equals("utc", StringComparison.OrdinalIgnoreCase)) { - // 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; - } + // Special case UTC, as previously ICU would convert it to "Etc/GMT" which is incorrect name for UTC. + ianaId = "Etc/UTC"; + return true; } - if (genericLocationName == genericName) + foreach (char c in windowsId) { - // 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; + // 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') + { + ianaId = null; + return false; + } } - // 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 + char* buffer = stackalloc char[100]; + int length = Interop.Globalization.WindowsIdToIanaId(windowsId, region, buffer, 100); + if (length > 0) { - // 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})"; + ianaId = allocate ? new string(buffer, 0, length) : null; + return true; } - } - - // 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('_', ' '); + ianaId = null; + return false; } - // 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.MinimalGlobalizationData.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs index 234d63366f3e1..5ef4d16edaa15 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.MinimalGlobalizationData.cs @@ -22,10 +22,23 @@ private static string GetUtcFullDisplayName(string timeZoneId, string standardDi return $"(UTC) {timeZoneId}"; } - private static string? GetAlternativeId(string id) + private static string? GetAlternativeId(string id, out bool idIsIana) { // No alternative IDs in this target. + idIsIana = false; return null; } + + private static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, bool allocate, out string? windowsId) + { + windowsId = null; + return false; + } + + private static unsafe bool TryConvertWindowsIdToIanaId(string windowsId, string? region, bool allocate, out string? ianaId) + { + ianaId = null; + return false; + } } } 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 6c7efd4b662b3..b664b1f990696 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -41,6 +41,8 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { _id = id; + HasIanaId = true; + // Handle UTC and its aliases if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) { 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 a13f48d6258de..c42bfabd6b74e 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -106,29 +106,10 @@ private static void PopulateAllSystemTimeZones(CachedData cachedData) } } - private static unsafe string? GetAlternativeId(string id) + private static string? GetAlternativeId(string id, out bool idIsIana) { - if (!GlobalizationMode.Invariant && !GlobalizationMode.UseNls) - { - 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.IanaIdToWindowsId(id, buffer, 100); - if (length > 0) - { - return new string(buffer, 0, length); - } - } - - return null; + idIsIana = true; + return TryConvertIanaIdToWindowsId(id, out string? windowsId) ? windowsId : null; } private TimeZoneInfo(in TIME_ZONE_INFORMATION zone, bool dstDisabled) diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 1ef3a1841ffd1..cb41702cdbd76 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Threading; @@ -136,6 +137,11 @@ public DateTimeKind GetCorrespondingKind(TimeZoneInfo? timeZone) public string Id => _id; + /// + /// Returns true if this TimeZoneInfo object has IANA Id. + /// + public bool HasIanaId { get; } + public string DisplayName => _displayName ?? string.Empty; public string StandardName => _standardDisplayName ?? string.Empty; @@ -880,7 +886,8 @@ private TimeZoneInfo( string? standardDisplayName, string? daylightDisplayName, AdjustmentRule[]? adjustmentRules, - bool disableDaylightSavingTime) + bool disableDaylightSavingTime, + bool hasIanaId = false) { ValidateTimeZoneInfo(id, baseUtcOffset, adjustmentRules, out bool adjustmentRulesSupportDst); @@ -891,6 +898,8 @@ private TimeZoneInfo( _daylightDisplayName = disableDaylightSavingTime ? null : daylightDisplayName; _supportsDaylightSavingTime = adjustmentRulesSupportDst && !disableDaylightSavingTime; _adjustmentRules = adjustmentRules; + + HasIanaId = _id.Equals(UtcId, StringComparison.OrdinalIgnoreCase) ? true : hasIanaId; } /// @@ -902,6 +911,8 @@ public static TimeZoneInfo CreateCustomTimeZone( string? displayName, string? standardDisplayName) { + bool hasIanaId = TimeZoneInfo.TryConvertIanaIdToWindowsId(id, allocate: false, out string _); + return new TimeZoneInfo( id, baseUtcOffset, @@ -909,7 +920,8 @@ public static TimeZoneInfo CreateCustomTimeZone( standardDisplayName, standardDisplayName, adjustmentRules: null, - disableDaylightSavingTime: false); + disableDaylightSavingTime: false, + hasIanaId); } /// @@ -950,6 +962,8 @@ public static TimeZoneInfo CreateCustomTimeZone( adjustmentRules = (AdjustmentRule[])adjustmentRules.Clone(); } + bool hasIanaId = TimeZoneInfo.TryConvertIanaIdToWindowsId(id, allocate: false, out string _); + return new TimeZoneInfo( id, baseUtcOffset, @@ -957,9 +971,35 @@ public static TimeZoneInfo CreateCustomTimeZone( standardDisplayName, daylightDisplayName, adjustmentRules, - disableDaylightSavingTime); + disableDaylightSavingTime, + hasIanaId); } + /// + /// Tries to convert IANA time zone Id to Windows Id. + /// + /// The IANA time zone Id. + /// String object hold the Windows Id which resulted from the IANA Id conversion. + /// True if the Id conversion succeed, false otherwise . + public static unsafe bool TryConvertIanaIdToWindowsId(string ianaId, [NotNullWhen(true)] out string? windowsId) => TryConvertIanaIdToWindowsId(ianaId, allocate: true, out windowsId); + + /// + /// Tries to convert Windows time zone Id to IANA Id. + /// + /// The Windows time zone Id. + /// String object hold the IANA Id which resulted from the Windows Id conversion. + /// True if the Id conversion succeed, false otherwise . + public static bool TryConvertWindowsIdToIanaId(string windowsId, [NotNullWhen(true)] out string? ianaId) => TryConvertWindowsIdToIanaId(windowsId, region: null, allocate: true, out ianaId); + + /// + /// Tries to convert Windows time zone Id to IANA Id. + /// + /// The Windows time zone Id. + /// The ISO 3166 for the country/region. + /// String object hold the IANA Id which resulted from the Windows Id conversion. + /// True if the Id conversion succeed, false otherwise . + public static unsafe bool TryConvertWindowsIdToIanaId(string windowsId, string? region, [NotNullWhen(true)] out string? ianaId) => TryConvertWindowsIdToIanaId(windowsId, region, allocate: true, out ianaId); + void IDeserializationCallback.OnDeserialization(object? sender) { try @@ -1794,7 +1834,7 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, bool dstDisabled, ou TimeZoneInfoResult result = TryGetTimeZoneUsingId(id, dstDisabled, out value, out e, cachedData, alwaysFallbackToLocalMachine); if (result != TimeZoneInfoResult.Success) { - string? alternativeId = GetAlternativeId(id); + string? alternativeId = GetAlternativeId(id, out bool idIsIana); if (alternativeId != null) { result = TryGetTimeZoneUsingId(alternativeId, dstDisabled, out value, out e, cachedData, alwaysFallbackToLocalMachine); @@ -1804,7 +1844,7 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, bool dstDisabled, ou if (value!._equivalentZones == null) { zone = new TimeZoneInfo(id, value!._baseUtcOffset, value!._displayName, value!._standardDisplayName, - value!._daylightDisplayName, value!._adjustmentRules, dstDisabled && value!._supportsDaylightSavingTime); + value!._daylightDisplayName, value!._adjustmentRules, dstDisabled && value!._supportsDaylightSavingTime, idIsIana); value!._equivalentZones = new List(); lock (value!._equivalentZones) { @@ -1824,7 +1864,7 @@ private static TimeZoneInfoResult TryGetTimeZone(string id, bool dstDisabled, ou if (zone == null) { zone = new TimeZoneInfo(id, value!._baseUtcOffset, value!._displayName, value!._standardDisplayName, - value!._daylightDisplayName, value!._adjustmentRules, dstDisabled && value!._supportsDaylightSavingTime); + value!._daylightDisplayName, value!._adjustmentRules, dstDisabled && value!._supportsDaylightSavingTime, idIsIana); lock (value!._equivalentZones) { value!._equivalentZones.Add(zone); @@ -1861,7 +1901,7 @@ private static TimeZoneInfoResult TryGetTimeZoneUsingId(string id, bool dstDisab else { value = new TimeZoneInfo(match._id, match._baseUtcOffset, match._displayName, match._standardDisplayName, - match._daylightDisplayName, match._adjustmentRules, disableDaylightSavingTime: false); + match._daylightDisplayName, match._adjustmentRules, disableDaylightSavingTime: false, match.HasIanaId); } return result; diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6292cc6b7e7db..c82f81f1e25fe 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -3851,6 +3851,7 @@ internal TimeZoneInfo() { } public System.TimeSpan BaseUtcOffset { get { throw null; } } public string DaylightName { get { throw null; } } public string DisplayName { get { throw null; } } + public bool HasIanaId { get; } public string Id { get { throw null; } } public static System.TimeZoneInfo Local { get { throw null; } } public string StandardName { get { throw null; } } @@ -3890,6 +3891,9 @@ void System.Runtime.Serialization.IDeserializationCallback.OnDeserialization(obj void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } public string ToSerializedString() { throw null; } public override string ToString() { throw null; } + public static bool TryConvertIanaIdToWindowsId(string ianaId, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out string? windowsId) { throw null; } + public static bool TryConvertWindowsIdToIanaId(string windowsId, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out string? ianaId) { throw null; } + public static bool TryConvertWindowsIdToIanaId(string windowsId, string? region, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out string? ianaId) { throw null; } public sealed partial class AdjustmentRule : System.IEquatable, System.Runtime.Serialization.IDeserializationCallback, System.Runtime.Serialization.ISerializable { internal AdjustmentRule() { } diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index ea673dea727b9..1d90c0ea4b46c 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -2314,7 +2314,7 @@ public static IEnumerable SystemTimeZonesTestData() for (int i = -14; i <= 12; i++) { TimeZoneInfo tz = null; - + try { string id = $"Etc/GMT{i:+0;-0}"; @@ -2638,6 +2638,65 @@ public static void UsingAlternativeTimeZoneIdsTest(string windowsId, string iana } } + public static bool SupportIanaNamesConversion => PlatformDetection.IsNotBrowser && PlatformDetection.ICUVersion.Major >= 52; + + [ConditionalFact(nameof(SupportIanaNamesConversion))] + public static void IsIanaIdTest() + { + bool expected = !s_isWindows; + + foreach (TimeZoneInfo tzi in TimeZoneInfo.GetSystemTimeZones()) + { + Assert.True((expected || tzi.Id.Equals("Utc", StringComparison.OrdinalIgnoreCase)) == tzi.HasIanaId, $"`{tzi.Id}` has wrong IANA Id indicator"); + } + + Assert.False(TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time").HasIanaId, $"`Pacific Standard Time` should not be IANA Id."); + Assert.True(TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles").HasIanaId, $"'America/Los_Angeles' should be IANA Id"); + } + + [ConditionalTheory(nameof(SupportIanaNamesConversion))] + [InlineData("Pacific Standard Time", "America/Los_Angeles")] + [InlineData("AUS Eastern Standard Time", "Australia/Sydney")] + [InlineData("GMT Standard Time", "Europe/London")] + [InlineData("Tonga Standard Time", "Pacific/Tongatapu")] + [InlineData("W. Australia Standard Time", "Australia/Perth")] + [InlineData("E. South America Standard Time", "America/Sao_Paulo")] + [InlineData("E. Africa Standard Time", "Africa/Nairobi")] + [InlineData("W. Europe Standard Time", "Europe/Berlin")] + [InlineData("Russian Standard Time", "Europe/Moscow")] + [InlineData("Libya Standard Time", "Africa/Tripoli")] + [InlineData("South Africa Standard Time", "Africa/Johannesburg")] + [InlineData("Morocco Standard Time", "Africa/Casablanca")] + [InlineData("Argentina Standard Time", "America/Buenos_Aires")] + [InlineData("Newfoundland Standard Time", "America/St_Johns")] + [InlineData("Iran Standard Time", "Asia/Tehran")] + public static void IdsConversionsTest(string windowsId, string ianaId) + { + Assert.True(TimeZoneInfo.TryConvertIanaIdToWindowsId(ianaId, out string winId)); + Assert.Equal(windowsId, winId); + + Assert.True(TimeZoneInfo.TryConvertWindowsIdToIanaId(winId, out string ianaConvertedId)); + Assert.Equal(ianaId, ianaConvertedId); + } + + [ConditionalTheory(nameof(SupportIanaNamesConversion))] + [InlineData("Pacific Standard Time", "America/Vancouver", "CA")] + [InlineData("Pacific Standard Time", "America/Los_Angeles", "US")] + [InlineData("Pacific Standard Time", "America/Los_Angeles", "\u0600NotValidRegion")] + [InlineData("Central Europe Standard Time", "Europe/Budapest", "DJ")] + [InlineData("Central Europe Standard Time", "Europe/Budapest", "\uFFFFNotValidRegion")] + [InlineData("Central Europe Standard Time", "Europe/Prague", "CZ")] + [InlineData("Central Europe Standard Time", "Europe/Ljubljana", "SI")] + [InlineData("Central Europe Standard Time", "Europe/Bratislava", "SK")] + [InlineData("Central Europe Standard Time", "Europe/Tirane", "AL")] + [InlineData("Central Europe Standard Time", "Europe/Podgorica", "ME")] + [InlineData("Central Europe Standard Time", "Europe/Belgrade", "RS")] + public static void IdsConversionsWithRegionTest(string windowsId, string ianaId, string region) + { + Assert.True(TimeZoneInfo.TryConvertWindowsIdToIanaId(windowsId, region, out string ianaConvertedId)); + Assert.Equal(ianaId, ianaConvertedId); + } + // We test the existence of a specific English time zone name to avoid failures on non-English platforms. [ConditionalFact(nameof(IsEnglishUILanguageAndRemoteExecutorSupported))] public static void TestNameWithInvariantCulture()