Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IANA To/From Windows Ids Conversion APIs #51093

Merged
merged 3 commits into from
Apr 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/libraries/Common/src/Interop/Interop.TimeZoneInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZone.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.AdjustmentRule.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.cs" Condition="'$(TargetsBrowser)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.StringSerializer.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.TransitionTime.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneNotFoundException.cs" />
Expand Down Expand Up @@ -1902,7 +1903,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.OSVersion.Unix.cs" Condition="'$(IsOSXLike)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Environment.SunOS.cs" Condition="'$(Targetsillumos)' == 'true' or '$(TargetsSolaris)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Threading\ThreadPoolWorkQueue.AutoreleasePool.OSX.cs" Condition="'$(IsOSXLike)' == 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\TimeZoneInfo.FullGlobalizationData.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\DriveInfoInternal.Unix.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IO\PersistedFiles.Unix.cs" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see in some place such culture sensitive string comparisons. Make sense to use ordinal?

(Below I see if (windowsId.Equals("utc", StringComparison.OrdinalIgnoreCase)))

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why we use ref string? displayName. Can it be "out":

Suggested change
private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName)
private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, out string? displayName)

{
if (GlobalizationMode.Invariant)
{
return;
}

string? timeZoneDisplayName;
bool result = Interop.CallStringMethod(
(buffer, locale, id, type) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we set static?

Suggested change
(buffer, locale, id, type) =>
static (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) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we set static?

Suggested change
(buffer, locale, id, type) =>
static (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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe swap conditions:

Suggested change
if (uiCulture.CompareInfo.IndexOf(genericName, exemplarCityName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0 && genericLocationName != null)
if (genericLocationName != null && uiCulture.CompareInfo.IndexOf(genericName, exemplarCityName, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0)

{
// 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;
}
}
}
Loading