Skip to content

Commit defd712

Browse files
authored
IANA To/From Windows Ids Conversion APIs (#51093)
* Iana To/From Windows Ids Conversion APIs * Address the feedback and fix the browser build breaks * Address the feedback
1 parent f24879f commit defd712

File tree

12 files changed

+421
-265
lines changed

12 files changed

+421
-265
lines changed

src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ internal static extern unsafe ResultCode GetTimeZoneDisplayName(
1616
int resultLength);
1717

1818
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_WindowsIdToIanaId")]
19-
internal static extern unsafe int WindowsIdToIanaId(string windowsId, char* ianaId, int ianaIdLength);
19+
internal static extern unsafe int WindowsIdToIanaId(string windowsId, [MarshalAs(UnmanagedType.LPStr)] string? region, char* ianaId, int ianaIdLength);
2020

2121
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_IanaIdToWindowsId")]
2222
internal static extern unsafe int IanaIdToWindowsId(string ianaId, char* windowsId, int windowsIdLength);

src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ static const UChar EXEMPLAR_CITY_PATTERN_UCHAR[] = {'V', 'V', 'V', '\0'};
2121
/*
2222
Convert Windows Time Zone Id to IANA Id
2323
*/
24-
int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, UChar* ianaId, int32_t ianaIdLength)
24+
int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength)
2525
{
2626
UErrorCode status = U_ZERO_ERROR;
2727

2828
if (ucal_getTimeZoneIDForWindowsID_ptr != NULL)
2929
{
30-
int32_t ianaIdFilledLength = ucal_getTimeZoneIDForWindowsID(windowsId, -1, NULL, ianaId, ianaIdLength, &status);
30+
int32_t ianaIdFilledLength = ucal_getTimeZoneIDForWindowsID(windowsId, -1, region, ianaId, ianaIdLength, &status);
3131
if (U_SUCCESS(status))
3232
{
3333
return ianaIdFilledLength;
@@ -91,7 +91,7 @@ static void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar*
9191
if (U_SUCCESS(*err))
9292
{
9393
udat_format(dateFormatter, timestamp, result, resultLength, NULL, err);
94-
udat_close(dateFormatter);
94+
udat_close(dateFormatter);
9595
}
9696
}
9797

src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ typedef enum
2020
TimeZoneDisplayName_ExemplarCity = 4,
2121
} TimeZoneDisplayNameType;
2222

23-
PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, UChar* ianaId, int32_t ianaIdLength);
23+
PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, const char* region, UChar* ianaId, int32_t ianaIdLength);
2424
PALEXPORT int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* windowsId, int32_t windowsIdLength);
2525
PALEXPORT ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength);

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,7 @@
10461046
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZone.cs" />
10471047
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.AdjustmentRule.cs" />
10481048
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.cs" />
1049+
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.cs" Condition="'$(TargetsBrowser)' != 'true'" />
10491050
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.StringSerializer.cs" />
10501051
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.TransitionTime.cs" />
10511052
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneNotFoundException.cs" />
@@ -1904,7 +1905,7 @@
19041905
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.OSVersion.Unix.cs" Condition="'$(IsOSXLike)' != 'true'" />
19051906
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.SunOS.cs" Condition="'$(Targetsillumos)' == 'true' or '$(TargetsSolaris)' == 'true'" />
19061907
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\ThreadPoolWorkQueue.AutoreleasePool.OSX.cs" Condition="'$(IsOSXLike)' == 'true'" />
1907-
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.cs" />
1908+
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.Unix.cs" />
19081909
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Unix.cs" />
19091910
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PersistedFiles.Unix.cs" />
19101911
</ItemGroup>
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Globalization;
5+
6+
namespace System
7+
{
8+
public sealed partial class TimeZoneInfo
9+
{
10+
private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time";
11+
private const string FallbackCultureName = "en-US";
12+
private const string GmtId = "GMT";
13+
14+
// Some time zones may give better display names using their location names rather than their generic name.
15+
// We can update this list as need arises.
16+
private static readonly string[] s_ZonesThatUseLocationName = new[] {
17+
"Europe/Minsk", // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)"
18+
"Europe/Moscow", // Prefer "Moscow Time" over "Moscow Standard Time"
19+
"Europe/Simferopol", // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)"
20+
"Pacific/Apia", // Prefer "Samoa Time" over "Apia Time"
21+
"Pacific/Pitcairn" // Prefer "Pitcairn Islands Time" over "Pitcairn Time"
22+
};
23+
24+
// Main function that is called during construction to populate the three display names
25+
private static void TryPopulateTimeZoneDisplayNamesFromGlobalizationData(string timeZoneId, TimeSpan baseUtcOffset, ref string? standardDisplayName, ref string? daylightDisplayName, ref string? displayName)
26+
{
27+
// Determine the culture to use
28+
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
29+
if (uiCulture.Name.Length == 0)
30+
uiCulture = CultureInfo.GetCultureInfo(FallbackCultureName); // ICU doesn't work nicely with InvariantCulture
31+
32+
// Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data.
33+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName);
34+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref daylightDisplayName);
35+
GetFullValueForDisplayNameField(timeZoneId, baseUtcOffset, uiCulture, ref displayName);
36+
}
37+
38+
// Helper function to get the standard display name for the UTC static time zone instance
39+
private static string GetUtcStandardDisplayName()
40+
{
41+
// Don't bother looking up the name for invariant or English cultures
42+
CultureInfo uiCulture = CultureInfo.CurrentUICulture;
43+
if (GlobalizationMode.Invariant || uiCulture.Name.Length == 0 || uiCulture.TwoLetterISOLanguageName == "en")
44+
return InvariantUtcStandardDisplayName;
45+
46+
// Try to get a localized version of "Coordinated Universal Time" from the globalization data
47+
string? standardDisplayName = null;
48+
GetDisplayName(UtcId, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref standardDisplayName);
49+
50+
// Final safety check. Don't allow null or abbreviations
51+
if (standardDisplayName == null || standardDisplayName == "GMT" || standardDisplayName == "UTC")
52+
standardDisplayName = InvariantUtcStandardDisplayName;
53+
54+
return standardDisplayName;
55+
}
56+
57+
// Helper function to get the full display name for the UTC static time zone instance
58+
private static string GetUtcFullDisplayName(string timeZoneId, string standardDisplayName)
59+
{
60+
return $"(UTC) {standardDisplayName}";
61+
}
62+
63+
// Helper function that retrieves various forms of time zone display names from ICU
64+
private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName)
65+
{
66+
if (GlobalizationMode.Invariant)
67+
{
68+
return;
69+
}
70+
71+
string? timeZoneDisplayName;
72+
bool result = Interop.CallStringMethod(
73+
(buffer, locale, id, type) =>
74+
{
75+
fixed (char* bufferPtr = buffer)
76+
{
77+
return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length);
78+
}
79+
},
80+
uiCulture,
81+
timeZoneId,
82+
nameType,
83+
out timeZoneDisplayName);
84+
85+
if (!result && uiCulture != FallbackCultureName)
86+
{
87+
// Try to fallback using FallbackCultureName just in case we can make it work.
88+
result = Interop.CallStringMethod(
89+
(buffer, locale, id, type) =>
90+
{
91+
fixed (char* bufferPtr = buffer)
92+
{
93+
return Interop.Globalization.GetTimeZoneDisplayName(locale, id, type, bufferPtr, buffer.Length);
94+
}
95+
},
96+
FallbackCultureName,
97+
timeZoneId,
98+
nameType,
99+
out timeZoneDisplayName);
100+
}
101+
102+
// If there is an unknown error, don't set the displayName field.
103+
// It will be set to the abbreviation that was read out of the tzfile.
104+
if (result && !string.IsNullOrEmpty(timeZoneDisplayName))
105+
{
106+
displayName = timeZoneDisplayName;
107+
}
108+
}
109+
110+
// Helper function that builds the value backing the DisplayName field from globalization data.
111+
private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, CultureInfo uiCulture, ref string? displayName)
112+
{
113+
// There are a few diffent ways we might show the display name depending on the data.
114+
// The algorithm used below should avoid duplicating the same words while still achieving the
115+
// goal of providing a unique, discoverable, and intuitive name.
116+
117+
// Try to get the generic name for this time zone.
118+
string? genericName = null;
119+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName);
120+
if (genericName == null)
121+
{
122+
// We'll use the fallback display name value already set.
123+
return;
124+
}
125+
126+
// Get the base offset to prefix in front of the time zone.
127+
// Only UTC and its aliases have "(UTC)", handled earlier. All other zones include an offset, even if it's zero.
128+
string baseOffsetText = $"(UTC{(baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{baseUtcOffset:hh\\:mm})";
129+
130+
// Get the generic location name.
131+
string? genericLocationName = null;
132+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref genericLocationName);
133+
134+
// Some edge cases only apply when the offset is +00:00.
135+
if (baseUtcOffset == TimeSpan.Zero)
136+
{
137+
// GMT and its aliases will just use the equivalent of "Greenwich Mean Time".
138+
string? gmtLocationName = null;
139+
GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref gmtLocationName);
140+
if (genericLocationName == gmtLocationName)
141+
{
142+
displayName = $"{baseOffsetText} {genericName}";
143+
return;
144+
}
145+
146+
// Other zones with a zero offset and the equivalent of "Greenwich Mean Time" should only use the location name.
147+
// For example, prefer "Iceland Time" over "Greenwich Mean Time (Reykjavik)".
148+
string? gmtGenericName = null;
149+
GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref gmtGenericName);
150+
if (genericName == gmtGenericName)
151+
{
152+
displayName = $"{baseOffsetText} {genericLocationName}";
153+
return;
154+
}
155+
}
156+
157+
if (genericLocationName == genericName)
158+
{
159+
// When the location name is the same as the generic name,
160+
// then it is generally good enough to show by itself.
161+
162+
// *** Example (en-US) ***
163+
// id = "America/Havana"
164+
// baseOffsetText = "(UTC-05:00)"
165+
// standardName = "Cuba Standard Time"
166+
// genericName = "Cuba Time"
167+
// genericLocationName = "Cuba Time"
168+
// exemplarCityName = "Havana"
169+
// displayName = "(UTC-05:00) Cuba Time"
170+
171+
displayName = $"{baseOffsetText} {genericLocationName}";
172+
return;
173+
}
174+
175+
// Prefer location names in some special cases.
176+
if (StringArrayContains(timeZoneId, s_ZonesThatUseLocationName, StringComparison.OrdinalIgnoreCase))
177+
{
178+
displayName = $"{baseOffsetText} {genericLocationName}";
179+
return;
180+
}
181+
182+
// See if we should include the exemplar city name.
183+
string exemplarCityName = GetExemplarCityName(timeZoneId, uiCulture.Name);
184+
if (uiCulture.CompareInfo.IndexOf(genericName, exemplarCityName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0 && genericLocationName != null)
185+
{
186+
// When an exemplar city is already part of the generic name,
187+
// there's no need to repeat it again so just use the generic name.
188+
189+
// *** Example (fr-FR) ***
190+
// id = "Australia/Lord_Howe"
191+
// baseOffsetText = "(UTC+10:30)"
192+
// standardName = "heure normale de Lord Howe"
193+
// genericName = "heure de Lord Howe"
194+
// genericLocationName = "heure : Lord Howe"
195+
// exemplarCityName = "Lord Howe"
196+
// displayName = "(UTC+10:30) heure de Lord Howe"
197+
198+
displayName = $"{baseOffsetText} {genericName}";
199+
}
200+
else
201+
{
202+
// Finally, use the generic name and the exemplar city together.
203+
// This provides an intuitive name and still disambiguates.
204+
205+
// *** Example (en-US) ***
206+
// id = "Europe/Rome"
207+
// baseOffsetText = "(UTC+01:00)"
208+
// standardName = "Central European Standard Time"
209+
// genericName = "Central European Time"
210+
// genericLocationName = "Italy Time"
211+
// exemplarCityName = "Rome"
212+
// displayName = "(UTC+01:00) Central European Time (Rome)"
213+
214+
displayName = $"{baseOffsetText} {genericName} ({exemplarCityName})";
215+
}
216+
}
217+
218+
// Helper function that gets an exmplar city name either from ICU or from the IANA time zone ID itself
219+
private static string GetExemplarCityName(string timeZoneId, string uiCultureName)
220+
{
221+
// First try to get the name through the localization data.
222+
string? exemplarCityName = null;
223+
GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.ExemplarCity, uiCultureName, ref exemplarCityName);
224+
if (!string.IsNullOrEmpty(exemplarCityName))
225+
return exemplarCityName;
226+
227+
// Support for getting exemplar city names was added in ICU 51.
228+
// We may have an older version. For example, in Helix we test on RHEL 7.5 which uses ICU 50.1.2.
229+
// We'll fallback to using an English name generated from the time zone ID.
230+
int i = timeZoneId.LastIndexOf('/');
231+
return timeZoneId.Substring(i + 1).Replace('_', ' ');
232+
}
233+
234+
// Helper function that returns an alternative ID using ICU data. Used primarily for converting from Windows IDs.
235+
private static unsafe string? GetAlternativeId(string id, out bool idIsIana)
236+
{
237+
idIsIana = false;
238+
return TryConvertWindowsIdToIanaId(id, null, out string? ianaId) ? ianaId : null;
239+
}
240+
}
241+
}

0 commit comments

Comments
 (0)