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