From e6ccd171a60bbcac29cbcbbf1807f1029116d449 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Thu, 25 Feb 2021 22:01:41 -0800 Subject: [PATCH 1/8] Improve time zone display names on Unix Fixes #16232 --- .../Interop.TimeZoneDisplayNameType.cs | 2 + .../pal_icushim_internal.h | 16 +- .../pal_icushim_internal_android.h | 23 +- .../pal_timeZoneInfo.c | 274 ++++++++++++++++-- .../pal_timeZoneInfo.h | 9 +- .../src/System/TimeZoneInfo.GetDisplayName.cs | 7 +- .../src/System/TimeZoneInfo.Unix.cs | 196 +++++++++++-- .../src/System/TimeZoneInfo.Win32.cs | 38 +++ .../src/System/TimeZoneInfo.cs | 15 +- .../tests/BinaryFormatterTests.cs | 12 + .../tests/System/TimeZoneInfoTests.cs | 195 +++++++++---- 11 files changed, 671 insertions(+), 116 deletions(-) diff --git a/src/libraries/Common/src/Interop/Interop.TimeZoneDisplayNameType.cs b/src/libraries/Common/src/Interop/Interop.TimeZoneDisplayNameType.cs index f46072196eb91..570eb0eb4b1ec 100644 --- a/src/libraries/Common/src/Interop/Interop.TimeZoneDisplayNameType.cs +++ b/src/libraries/Common/src/Interop/Interop.TimeZoneDisplayNameType.cs @@ -11,6 +11,8 @@ internal enum TimeZoneDisplayNameType Generic = 0, Standard = 1, DaylightSavings = 2, + GenericLocation = 3, + ExemplarCity = 4, } } } diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h index 836ba44ec182f..1f426969ac78d 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h @@ -58,27 +58,33 @@ // (U_ICU_VERSION_MAJOR_NUM < 52) // The following APIs are not supported in the ICU versions less than 52. We need to define them manually. // We have to do runtime check before using the pointers to these APIs. That is why these are listed in the FOR_ALL_OPTIONAL_ICU_FUNCTIONS list. -U_CAPI int32_t U_EXPORT2 ucal_getWindowsTimeZoneID(const UChar* id, int32_t len,UChar* winid, int32_t winidCapacity, UErrorCode* status); U_CAPI int32_t U_EXPORT2 ucal_getTimeZoneIDForWindowsID(const UChar* winid, int32_t len, const char* region, UChar* id, int32_t idCapacity, UErrorCode* status); +U_CAPI int32_t U_EXPORT2 ucal_getWindowsTimeZoneID(const UChar* id, int32_t len,UChar* winid, int32_t winidCapacity, UErrorCode* status); #endif // List of all functions from the ICU libraries that are used in the System.Globalization.Native.so #define FOR_ALL_UNCONDITIONAL_ICU_FUNCTIONS \ PER_FUNCTION_BLOCK(u_charsToUChars, libicuuc, true) \ PER_FUNCTION_BLOCK(u_getVersion, libicuuc, true) \ + PER_FUNCTION_BLOCK(u_strcmp, libicuuc, true) \ + PER_FUNCTION_BLOCK(u_strcpy, libicuuc, true) \ PER_FUNCTION_BLOCK(u_strlen, libicuuc, true) \ PER_FUNCTION_BLOCK(u_strncpy, libicuuc, true) \ PER_FUNCTION_BLOCK(u_tolower, libicuuc, true) \ PER_FUNCTION_BLOCK(u_toupper, libicuuc, true) \ + PER_FUNCTION_BLOCK(u_uastrcpy, libicuuc, true) \ PER_FUNCTION_BLOCK(ucal_add, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_close, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_get, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_getAttribute, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_getKeywordValuesForLocale, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_getLimit, libicui18n, true) \ + PER_FUNCTION_BLOCK(ucal_getNow, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_getTimeZoneDisplayName, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_open, libicui18n, true) \ + PER_FUNCTION_BLOCK(ucal_openTimeZoneIDEnumeration, libicui18n, true) \ PER_FUNCTION_BLOCK(ucal_set, libicui18n, true) \ + PER_FUNCTION_BLOCK(ucal_setMillis, libicui18n, true) \ PER_FUNCTION_BLOCK(ucol_close, libicui18n, true) \ PER_FUNCTION_BLOCK(ucol_closeElements, libicui18n, true) \ PER_FUNCTION_BLOCK(ucol_getOffset, libicui18n, true) \ @@ -96,6 +102,7 @@ U_CAPI int32_t U_EXPORT2 ucal_getTimeZoneIDForWindowsID(const UChar* winid, int3 PER_FUNCTION_BLOCK(ucol_strcoll, libicui18n, true) \ PER_FUNCTION_BLOCK(udat_close, libicui18n, true) \ PER_FUNCTION_BLOCK(udat_countSymbols, libicui18n, true) \ + PER_FUNCTION_BLOCK(udat_format, libicui18n, true) \ PER_FUNCTION_BLOCK(udat_getSymbols, libicui18n, true) \ PER_FUNCTION_BLOCK(udat_open, libicui18n, true) \ PER_FUNCTION_BLOCK(udat_setCalendar, libicui18n, true) \ @@ -202,21 +209,27 @@ FOR_ALL_ICU_FUNCTIONS // to the functions of the selected version of ICU in the initialization. #define u_charsToUChars(...) u_charsToUChars_ptr(__VA_ARGS__) #define u_getVersion(...) u_getVersion_ptr(__VA_ARGS__) +#define u_strcmp(...) u_strcmp_ptr(__VA_ARGS__) +#define u_strcpy(...) u_strcpy_ptr(__VA_ARGS__) #define u_strlen(...) u_strlen_ptr(__VA_ARGS__) #define u_strncpy(...) u_strncpy_ptr(__VA_ARGS__) #define u_tolower(...) u_tolower_ptr(__VA_ARGS__) #define u_toupper(...) u_toupper_ptr(__VA_ARGS__) +#define u_uastrcpy(...) u_uastrcpy_ptr(__VA_ARGS__) #define ucal_add(...) ucal_add_ptr(__VA_ARGS__) #define ucal_close(...) ucal_close_ptr(__VA_ARGS__) #define ucal_get(...) ucal_get_ptr(__VA_ARGS__) #define ucal_getAttribute(...) ucal_getAttribute_ptr(__VA_ARGS__) #define ucal_getKeywordValuesForLocale(...) ucal_getKeywordValuesForLocale_ptr(__VA_ARGS__) #define ucal_getLimit(...) ucal_getLimit_ptr(__VA_ARGS__) +#define ucal_getNow(...) ucal_getNow_ptr(__VA_ARGS__) #define ucal_getTimeZoneDisplayName(...) ucal_getTimeZoneDisplayName_ptr(__VA_ARGS__) #define ucal_getTimeZoneIDForWindowsID(...) ucal_getTimeZoneIDForWindowsID_ptr(__VA_ARGS__) #define ucal_getWindowsTimeZoneID(...) ucal_getWindowsTimeZoneID_ptr(__VA_ARGS__) #define ucal_open(...) ucal_open_ptr(__VA_ARGS__) +#define ucal_openTimeZoneIDEnumeration(...) ucal_openTimeZoneIDEnumeration_ptr(__VA_ARGS__) #define ucal_set(...) ucal_set_ptr(__VA_ARGS__) +#define ucal_setMillis(...) ucal_setMillis_ptr(__VA_ARGS__) #define ucol_close(...) ucol_close_ptr(__VA_ARGS__) #define ucol_closeElements(...) ucol_closeElements_ptr(__VA_ARGS__) #define ucol_getOffset(...) ucol_getOffset_ptr(__VA_ARGS__) @@ -241,6 +254,7 @@ FOR_ALL_ICU_FUNCTIONS #define ucurr_getName(...) ucurr_getName_ptr(__VA_ARGS__) #define udat_close(...) udat_close_ptr(__VA_ARGS__) #define udat_countSymbols(...) udat_countSymbols_ptr(__VA_ARGS__) +#define udat_format(...) udat_format_ptr(__VA_ARGS__) #define udat_getSymbols(...) udat_getSymbols_ptr(__VA_ARGS__) #define udat_open(...) udat_open_ptr(__VA_ARGS__) #define udat_setCalendar(...) udat_setCalendar_ptr(__VA_ARGS__) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h index 7e05fa35d03ca..fea4c8696f785 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h @@ -25,6 +25,7 @@ typedef struct UBreakIterator UBreakIterator; typedef int8_t UBool; typedef uint16_t UChar; typedef int32_t UChar32; +typedef double UDate; typedef uint8_t UVersionInfo[U_MAX_VERSION_LENGTH]; typedef void* UNumberFormat; @@ -369,6 +370,12 @@ typedef enum UCollationResult { UCOL_LESS = -1 } UCollationResult; +typedef enum USystemTimeZoneType { + UCAL_ZONE_TYPE_ANY, + UCAL_ZONE_TYPE_CANONICAL, + UCAL_ZONE_TYPE_CANONICAL_LOCATION +} USystemTimeZoneType; + enum { UIDNA_ERROR_EMPTY_LABEL = 1, UIDNA_ERROR_LABEL_TOO_LONG = 2, @@ -419,25 +426,36 @@ typedef struct UIDNAInfo { int32_t reservedI3; } UIDNAInfo; +typedef struct UFieldPosition { + int32_t field; + int32_t beginIndex; + int32_t endIndex; +} UFieldPosition; void u_charsToUChars(const char * cs, UChar * us, int32_t length); void u_getVersion(UVersionInfo versionArray); int32_t u_strlen(const UChar * s); +int32_t u_strcmp(const UChar * s1, const UChar * s2); +UChar * u_strcpy(UChar * dst, const UChar * src); UChar * u_strncpy(UChar * dst, const UChar * src, int32_t n); UChar32 u_tolower(UChar32 c); UChar32 u_toupper(UChar32 c); +UChar* u_uastrcpy(UChar * dst, const char * src); void ucal_add(UCalendar * cal, UCalendarDateFields field, int32_t amount, UErrorCode * status); void ucal_close(UCalendar * cal); int32_t ucal_get(const UCalendar * cal, UCalendarDateFields field, UErrorCode * status); int32_t ucal_getAttribute(const UCalendar * cal, UCalendarAttribute attr); UEnumeration * ucal_getKeywordValuesForLocale(const char * key, const char * locale, UBool commonlyUsed, UErrorCode * status); int32_t ucal_getLimit(const UCalendar * cal, UCalendarDateFields field, UCalendarLimitType type, UErrorCode * status); +UDate ucal_getNow(void); int32_t ucal_getTimeZoneDisplayName(const UCalendar * cal, UCalendarDisplayNameType type, const char * locale, UChar * result, int32_t resultLength, UErrorCode * status); -UCalendar * ucal_open(const UChar * zoneID, int32_t len, const char * locale, UCalendarType type, UErrorCode * status); -void ucal_set(UCalendar * cal, UCalendarDateFields field, int32_t value); int32_t ucal_getTimeZoneIDForWindowsID(const UChar * winid, int32_t len, const char * region, UChar * id, int32_t idCapacity, UErrorCode * status); int32_t ucal_getWindowsTimeZoneID(const UChar * id, int32_t len, UChar * winid, int32_t winidCapacity, UErrorCode * status); +UCalendar * ucal_open(const UChar * zoneID, int32_t len, const char * locale, UCalendarType type, UErrorCode * status); +UEnumeration * ucal_openTimeZoneIDEnumeration(USystemTimeZoneType zoneType, const char * region, const int32_t * rawOffset, UErrorCode * ec); +void ucal_set(UCalendar * cal, UCalendarDateFields field, int32_t value); +void ucal_setMillis(UCalendar * cal, UDate dateTime, UErrorCode * status); void ucol_close(UCollator * coll); void ucol_closeElements(UCollationElements * elems); int32_t ucol_getOffset(const UCollationElements *elems); @@ -457,6 +475,7 @@ int32_t ucurr_forLocale(const char * locale, UChar * buff, int32_t buffCapacity, const UChar * ucurr_getName(const UChar * currency, const char * locale, UCurrNameStyle nameStyle, UBool * isChoiceFormat, int32_t * len, UErrorCode * ec); void udat_close(UDateFormat * format); int32_t udat_countSymbols(const UDateFormat * fmt, UDateFormatSymbolType type); +int32_t udat_format(const UDateFormat * format, UDate dateToFormat, UChar * result, int32_t resultLength, UFieldPosition * position, UErrorCode * status); int32_t udat_getSymbols(const UDateFormat * fmt, UDateFormatSymbolType type, int32_t symbolIndex, UChar * result, int32_t resultLength, UErrorCode * status); UDateFormat * udat_open(UDateFormatStyle timeStyle, UDateFormatStyle dateStyle, const char * locale, const UChar * tzID, int32_t tzIDLength, const UChar * pattern, int32_t patternLength, UErrorCode * status); void udat_setCalendar(UDateFormat * fmt, const UCalendar * calendarToSet); 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 09c2c1ca5092b..a6407f859eae0 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c @@ -4,41 +4,24 @@ #include #include +#include #include "pal_errors_internal.h" #include "pal_locale_internal.h" #include "pal_timeZoneInfo.h" -/* -Gets the localized display name for the specified time zone. -*/ -ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, - const UChar* timeZoneId, - TimeZoneDisplayNameType type, - UChar* result, - int32_t resultLength) -{ - UErrorCode err = U_ZERO_ERROR; - char locale[ULOC_FULLNAME_CAPACITY]; - GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err); - - int32_t timeZoneIdLength = -1; // timeZoneId is NULL-terminated - UCalendar* calendar = ucal_open(timeZoneId, timeZoneIdLength, locale, UCAL_DEFAULT, &err); +#define DISPLAY_NAME_LENGTH 256 // arbitrarily large, to be safe +#define TZID_LENGTH 64 // arbitrarily large, to be safe - // TODO (https://github.com/dotnet/runtime/issues/16232): need to support Generic names, but ICU "C" api - // has no public option for this. For now, just use the ICU standard name for both Standard and Generic - // (which is the same behavior on Windows with the mincore TIME_ZONE_INFORMATION APIs). - ucal_getTimeZoneDisplayName( - calendar, - type == TimeZoneDisplayName_DaylightSavings ? UCAL_DST : UCAL_STANDARD, - locale, - result, - resultLength, - &err); +// For descriptions of the following patterns, see https://unicode-org.github.io/icu/userguide/format_parse/datetime/#date-field-symbol-table +static const UChar GENERIC_PATTERN_UCHAR[] = {'v', 'v', 'v', 'v', '\0'}; // u"vvvv" +static const UChar GENERIC_LOCATION_PATTERN_UCHAR[] = {'V', 'V', 'V', 'V', '\0'}; // u"VVVV" +static const UChar EXEMPLAR_CITY_PATTERN_UCHAR[] = {'V', 'V', 'V', '\0'}; // u"VVV" - ucal_close(calendar); - return GetResultCode(err); -} +// Declare some private functions, implemented later in this file. +void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZoneId, const UDate timestamp, UCalendarDisplayNameType type, UChar* result, int32_t resultLength, UErrorCode* err); +void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZoneId, const UDate timestamp, const UChar* pattern, UChar* result, int32_t resultLength, UErrorCode* err); +void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId, const UDate timestamp, UChar* genericName, UErrorCode* err); /* Convert Windows Time Zone Id to IANA Id @@ -80,3 +63,238 @@ int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* window // Failed return 0; } + +/* +Gets the localized display name that is currently in effect for the specified time zone. +*/ +ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength) +{ + UErrorCode err = U_ZERO_ERROR; + char locale[ULOC_FULLNAME_CAPACITY]; + GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err); + + // Note: Due to how CLDR Metazones work, a past or future timestamp might use a different set of display names + // than are currently in effect. + // + // See https://github.com/unicode-org/cldr/blob/master/common/supplemental/metaZones.xml + // + // Example: As of writing this, Africa/Algiers is in the Europe_Central metazone, + // which has a standard-time name of "Central European Standard Time" (in English). + // However, in some previous dates, it used the Europe_Western metazone, + // having the standard-time name of "Western European Standard Time" (in English). + // Only the *current* name will be returned. + // + // TODO: Add a parameter for the timestamp that is used when getting the display names instead of + // getting "now" on the following line. Everything else should be using this timestamp. + // For now, since TimeZoneInfo presently uses only a single set of display names, we will + // use the names associated with the *current* date and time. + + UDate timestamp = ucal_getNow(); + + switch (type) + { + case TimeZoneDisplayName_Standard: + GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_STANDARD, result, resultLength, &err); + break; + + case TimeZoneDisplayName_DaylightSavings: + GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_DST, result, resultLength, &err); + break; + + case TimeZoneDisplayName_Generic: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_PATTERN_UCHAR, result, resultLength, &err); + if (U_SUCCESS(err)) + { + FixupTimeZoneGenericDisplayName(locale, timeZoneId, timestamp, result, &err); + } + break; + + case TimeZoneDisplayName_GenericLocation: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, result, resultLength, &err); + break; + + case TimeZoneDisplayName_ExemplarCity: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, EXEMPLAR_CITY_PATTERN_UCHAR, result, resultLength, &err); + break; + + default: + return UnknownError; + } + + return GetResultCode(err); +} + +/* +Private function to get the standard and daylight names from the ICU Calendar API. +*/ +void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZoneId, const UDate timestamp, UCalendarDisplayNameType type, UChar* result, int32_t resultLength, UErrorCode* err) +{ + // Examples: "Pacific Standard Time" (standard) + // "Pacific Daylight Time" (daylight) + + // (-1 == timeZoneId is null terminated) + UCalendar* calendar = ucal_open(timeZoneId, -1, locale, UCAL_DEFAULT, err); + if (U_FAILURE(*err)) goto exit; + + ucal_setMillis(calendar, timestamp, err); + if (U_FAILURE(*err)) goto exit; + + ucal_getTimeZoneDisplayName(calendar, type, locale, result, resultLength, err); + + exit: + ucal_close(calendar); +} + +/* +Private function to get the various forms of generic time zone names using patterns with the ICU Date Formatting API. +*/ +void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZoneId, const UDate timestamp, const UChar* pattern, UChar* result, int32_t resultLength, UErrorCode* err) +{ + // (-1 == timeZoneId and pattern are null terminated) + UDateFormat* dateFormatter = udat_open(UDAT_PATTERN, UDAT_PATTERN, locale, timeZoneId, -1, pattern, -1, err); + if (U_FAILURE(*err)) goto exit; + + udat_format(dateFormatter, timestamp, result, resultLength, NULL, err); + + exit: + udat_close(dateFormatter); +} + +/* +Private function to modify the generic display name to better suit our needs. +*/ +void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId, const UDate timestamp, UChar* genericName, UErrorCode* err) +{ + // By default, some time zones will still give a standard name instead of the generic + // non-location name. + // + // For example, given the following zones and their English results: + // America/Denver => "Mountain Time" + // America/Phoenix => "Mountain Standard Time" + // + // We prefer that all time zones in the same metazone have the same generic name, + // such that they are grouped together when combined with their base offset, location + // and sorted alphabetically. For example: + // + // (UTC-07:00) Mountain Time (Denver) + // (UTC-07:00) Mountain Time (Phoenix) + // + // Without modification, they would show as: + // + // (UTC-07:00) Mountain Standard Time (Phoenix) + // (UTC-07:00) Mountain Time (Denver) + // + // When combined with the rest of the time zones, having them not grouped together + // makes it harder to locate the correct time zone from a list. + // + // The reason we get the standard name is because TR35 (LDML) defines a rule that + // states that metazone generic names should use standard names if there is no DST + // transition within a +/- 184 day range near the timestamp being translated. + // + // See the "Type Fallback" section in: + // https://www.unicode.org/reports/tr35/tr35-dates.html#Using_Time_Zone_Names + // + // This might make sense when attached to an exact timestamp, but doesn't work well + // when using the generic name to pick a time zone from a list. + // Note that this test only happens when the generic name comes from a metazone. + // + // ICU implements this test in TZGNCore::formatGenericNonLocationName in + // https://github.com/unicode-org/icu/blob/master/icu4c/source/i18n/tzgnames.cpp + // (Note the kDstCheckRange 184-day constant.) + // + // The rest of the code below is a workaround for this issue. When the generic + // name and standard name match, we search through the other time zones for one + // having the same base offset and standard name but a shorter generic name. + // That will at least keep them grouped together, though note that if there aren't + // any found that means all of them are using the standard name. + // + // If ICU ever adds an API to get a generic name that doesn't perform the + // 184-day check on metazone names, then test for the existence of that new API + // and use that instead of this workaround. Keep the workaround for when the + // new API is not available. + + // Get the standard name for this time zone. (-1 == timeZoneId is null terminated) + // Note that we leave the calendar open and close it later so we can also get the base offset. + UChar standardName[DISPLAY_NAME_LENGTH]; + UCalendar* calendar = ucal_open(timeZoneId, -1, locale, UCAL_DEFAULT, err); + if (U_FAILURE(*err)) goto exit; + + ucal_setMillis(calendar, timestamp, err); + if (U_FAILURE(*err)) goto exit; + + ucal_getTimeZoneDisplayName(calendar, UCAL_STANDARD, locale, standardName, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) goto exit; + + // See if the generic name is the same as the standard name. + if (u_strcmp(genericName, standardName) == 0) + { + // Get some details for later comparison. + const int32_t originalGenericNameActualLength = u_strlen(genericName); + const int32_t baseOffset = ucal_get(calendar, UCAL_ZONE_OFFSET, err); + if (U_FAILURE(*err)) goto exit; + + // Allocate some additional strings for test values. + UChar testTimeZoneId[TZID_LENGTH]; + UChar testDisplayName[DISPLAY_NAME_LENGTH]; + UChar testDisplayName2[DISPLAY_NAME_LENGTH]; + + // Enumerate over all the time zones having the same base offset. + UEnumeration* pEnum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_CANONICAL_LOCATION, NULL, &baseOffset, err); + if (U_FAILURE(*err)) goto exitEnum; + + int count = uenum_count(pEnum, err); + if (U_FAILURE(*err)) goto exitEnum; + + for (int i = 0; i < count; i++) + { + // Get a time zone id from the enumeration to test with. + int32_t testIdLength; + const char* testId = uenum_next(pEnum, &testIdLength, err); + if (U_FAILURE(*err)) goto exitEnum; + u_uastrcpy(testTimeZoneId, testId); + + // Get the standard name from the test time zone. + GetTimeZoneDisplayName_FromCalendar(locale, testTimeZoneId, timestamp, UCAL_STANDARD, testDisplayName, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) goto exitEnum; + + // See if the test time zone has a different standard name. + if (u_strcmp(testDisplayName, standardName) != 0) + { + // It has a different standard name. We can't use it. + continue; + } + + // Get the generic name from the test time zone. + GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_PATTERN_UCHAR, testDisplayName, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) goto exitEnum; + + // See if the test time zone has a longer (or same size) generic name. + if (u_strlen(testDisplayName) >= originalGenericNameActualLength) + { + // The test time zone's generic name isn't any shorter than the one we already have. + continue; + } + + // We probably have found a better generic name. But just to be safe, make sure the test zone isn't + // using a generic name that is specific to a particular location. For example, "Antarctica/Troll" + // uses "Troll Time" as a generic name, but "Greenwich Mean Time" as a standard name. We don't + // want other zones that use "Greenwich Mean Time" to be labeled as "Troll Time". + + GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, testDisplayName2, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) goto exitEnum; + + if (u_strcmp(testDisplayName, testDisplayName2) != 0) + { + // We have found a better generic name. Use it. + u_strcpy(genericName, testDisplayName); + break; + } + } + + exitEnum: + uenum_close(pEnum); + } + + exit: + ucal_close(calendar); +} 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 c9fe188979c11..49bfb6250eb50 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.h @@ -16,13 +16,10 @@ typedef enum TimeZoneDisplayName_Generic = 0, TimeZoneDisplayName_Standard = 1, TimeZoneDisplayName_DaylightSavings = 2, + TimeZoneDisplayName_GenericLocation = 3, + TimeZoneDisplayName_ExemplarCity = 4, } TimeZoneDisplayNameType; -PALEXPORT ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, - const UChar* timeZoneId, - TimeZoneDisplayNameType type, - UChar* result, - int32_t resultLength); - PALEXPORT int32_t GlobalizationNative_WindowsIdToIanaId(const UChar* windowsId, 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/TimeZoneInfo.GetDisplayName.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs index 834b719fefe2c..ba54330cf8919 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.GetDisplayName.cs @@ -17,11 +17,10 @@ namespace System { public sealed partial class TimeZoneInfo { - private unsafe void GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName) + private static unsafe void GetDisplayName(string timeZoneId, Interop.Globalization.TimeZoneDisplayNameType nameType, string uiCulture, ref string? displayName) { if (GlobalizationMode.Invariant) { - displayName = _standardDisplayName; return; } @@ -35,7 +34,7 @@ private unsafe void GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType } }, uiCulture, - _id, + timeZoneId, nameType, out timeZoneDisplayName); @@ -51,7 +50,7 @@ private unsafe void GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType } }, FallbackCultureName, - _id, + timeZoneId, nameType, out timeZoneDisplayName); } 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 44c49e52de0b1..8b70260adc7fa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -23,6 +23,15 @@ public sealed partial class TimeZoneInfo private const string TimeZoneEnvironmentVariable = "TZ"; private const string TimeZoneDirectoryEnvironmentVariable = "TZDIR"; 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. + private const string ZonesThatUseLocationName = + "Europe/Minsk\n" + // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" + "Europe/Moscow\n" + // Prefer "Moscow Time" over "Moscow Standard Time" + "Europe/Simferopol\n" + // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" + "Pacific/Apia\n" + // Prefer "Samoa Time" over "Apia Time" + "Pacific/Pitcairn\n"; // Prefer "Pitcairn Islands Time" over "Pitcairn Time" private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { @@ -41,11 +50,10 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); _id = id; - _displayName = LocalId; _baseUtcOffset = TimeSpan.Zero; // find the best matching baseUtcOffset and display strings based on the current utcNow value. - // NOTE: read the display strings from the tzfile now in case they can't be loaded later + // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later // from the globalization data. DateTime utcNow = DateTime.UtcNow; for (int i = 0; i < dts.Length && dts[i] <= utcNow; i++) @@ -82,21 +90,18 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) // Use abbrev as the fallback _standardDisplayName = standardAbbrevName; - _daylightDisplayName = daylightAbbrevName; + _daylightDisplayName = daylightAbbrevName ?? standardAbbrevName; _displayName = _standardDisplayName; - string uiCulture = CultureInfo.CurrentUICulture.Name.Length == 0 ? FallbackCultureName : CultureInfo.CurrentUICulture.Name; // ICU doesn't work nicely with Invariant - GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture, ref _displayName); - GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture, ref _standardDisplayName); - GetDisplayName(Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture, ref _daylightDisplayName); + // 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 - if (_standardDisplayName == _displayName) - { - if (_baseUtcOffset >= TimeSpan.Zero) - _displayName = $"(UTC+{_baseUtcOffset:hh\\:mm}) {_standardDisplayName}"; - else - _displayName = $"(UTC-{_baseUtcOffset:hh\\:mm}) {_standardDisplayName}"; - } + // Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data. + GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref _standardDisplayName); + GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref _daylightDisplayName); + GetFullValueForDisplayNameField(_id, _baseUtcOffset, _standardDisplayName, uiCulture, ref _displayName); // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns // with DateTimeOffset, SQL Server, and the W3C XML Specification @@ -114,6 +119,144 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) ValidateTimeZoneInfo(_id, _baseUtcOffset, _adjustmentRules, out _supportsDaylightSavingTime); } + // Helper function that builds the value backing the DisplayName field from gloablization data. + private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, string? standardName, 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. + + string? utcStandardName = GetUtcStandardDisplayName(); + if (standardName == utcStandardName) + { + // This gives the display name for UTC and all of its aliases (Etc/UTC, Universal, etc.) + displayName = $"(UTC) {utcStandardName}"; + return; + } + + // Get the base offset to prefix in front of the time zone. + // Only UTC and its aliases have "(UTC)" per above. All other zones include an offset, even if it's zero. + string baseOffsetText = $"(UTC{(baseUtcOffset >= TimeSpan.Zero ? '+' : '-')}{baseUtcOffset:hh\\:mm})"; + + // Try to get the generic name for this time zone. + string? genericName = null; + if (!GlobalizationMode.Invariant) + { + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); + } + + if (genericName == null) + { + // When we can't get a generic name, use the offset and the ID. + // It is not ideal, but at least it is non-ambiguous. + // (Note, UTC was handled already above.) + displayName = $"{baseOffsetText} {timeZoneId}"; + return; + } + + // Get the generic location name. + string? genericLocationName = null; + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref genericLocationName); + + // Some edge cases only apply when the offset is +00:00. + if (baseUtcOffset == TimeSpan.Zero) + { + // GMT and its aliases will just use the equivalent of "Greenwich Mean Time". + string? gmtLocationName = null; + GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.GenericLocation, uiCulture.Name, ref gmtLocationName); + if (genericLocationName == gmtLocationName) + { + displayName = $"{baseOffsetText} {genericName}"; + return; + } + + // Other zones with a zero offset and the equivalent of "Greenwich Mean Time" should only use the location name. + // For example, prefer "Iceland Time" over "Greenwich Mean Time (Reykjavik)". + string? gmtGenericName = null; + GetDisplayName(GmtId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref gmtGenericName); + if (genericName == gmtGenericName) + { + displayName = $"{baseOffsetText} {genericLocationName}"; + return; + } + } + + if (genericLocationName == genericName) + { + // When the location name is the same as the generic name, + // then it is generally good enough to show by itself. + + // *** Example (en-US) *** + // id = "America/Havana" + // baseOffsetText = "(UTC-05:00)" + // standardName = "Cuba Standard Time" + // genericName = "Cuba Time" + // genericLocationName = "Cuba Time" + // exemplarCityName = "Havana" + // displayName = "(UTC-05:00) Cuba Time" + + displayName = $"{baseOffsetText} {genericLocationName}"; + return; + } + + // Also use location names in some special cases. (See the list at the top of this file.) + if (ZonesThatUseLocationName.Contains(timeZoneId, 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})"; + } + } + + 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('_', ' '); + } + // The TransitionTime fields are not used when AdjustmentRule.NoDaylightTransitions == true. // However, there are some cases in the past where DST = true, and the daylight savings offset // now equals what the current BaseUtcOffset is. In that case, the AdjustmentRule.DaylightOffset @@ -988,7 +1131,7 @@ private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZone baseUtcDelta, noDaylightTransitions: true); - if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r)) + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) { NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); } @@ -1031,7 +1174,7 @@ private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZone baseUtcDelta, noDaylightTransitions: true); - if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r)) + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) { NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); } @@ -1068,7 +1211,7 @@ private static void TZif_GenerateAdjustmentRule(ref int index, TimeSpan timeZone noDaylightTransitions: true); } - if (!IsValidAdjustmentRuleOffest(timeZoneBaseUtcOffset, r)) + if (!IsValidAdjustmentRuleOffset(timeZoneBaseUtcOffset, r)) { NormalizeAdjustmentRuleOffset(timeZoneBaseUtcOffset, ref r); } @@ -1789,5 +1932,24 @@ private enum TZVersion : byte V3, // when adding more versions, ensure all the logic using TZVersion is still correct } + + // 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; + } } } 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 6223e66e018ae..8cc7ee8bb88f4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -1000,5 +1000,43 @@ private static TimeZoneInfoResult TryGetTimeZoneFromLocalMachine(string id, out } } } + + // 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 (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; + using (RegistryKey? key = Registry.LocalMachine.OpenSubKey(TimeZonesRegistryHive + "\\" + UtcId, writable: false)) + { + if (key != null) + { + // read the MUI_ registry key + string? standardNameMuiResource = key.GetValue(MuiStandardValue, string.Empty) as string; + + // try to load the string from the native resource DLL(s) + if (!string.IsNullOrEmpty(standardNameMuiResource)) + { + standardDisplayName = TryGetLocalizedNameByMuiNativeResource(standardNameMuiResource); + } + + // fallback to using the standard registry key + if (string.IsNullOrEmpty(standardDisplayName)) + { + standardDisplayName = key.GetValue(StandardValue, string.Empty) as string; + } + } + } + + // Final safety check. Don't allow null or abbreviations + if (standardDisplayName == null || standardDisplayName == "GMT" || standardDisplayName == "UTC") + standardDisplayName = InvariantUtcStandardDisplayName; + + return standardDisplayName; + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 771cef56a3a8c..90a425e4f672f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -52,8 +52,9 @@ private enum TimeZoneInfoResult // constants for TimeZoneInfo.Local and TimeZoneInfo.Utc private const string UtcId = "UTC"; private const string LocalId = "Local"; + private const string InvariantUtcStandardDisplayName = "Coordinated Universal Time"; - private static readonly TimeZoneInfo s_utcTimeZone = CreateCustomTimeZone(UtcId, TimeSpan.Zero, "(UTC) Coordinated Universal Time", "Coordinated Universal Time"); + private static readonly TimeZoneInfo s_utcTimeZone = CreateUtcTimeZone(); private static CachedData s_cachedData = new CachedData(); @@ -1976,7 +1977,7 @@ private static void ValidateTimeZoneInfo(string id, TimeSpan baseUtcOffset, Adju throw new InvalidTimeZoneException(SR.Argument_AdjustmentRulesNoNulls); } - if (!IsValidAdjustmentRuleOffest(baseUtcOffset, current)) + if (!IsValidAdjustmentRuleOffset(baseUtcOffset, current)) { throw new InvalidTimeZoneException(SR.ArgumentOutOfRange_UtcOffsetAndDaylightDelta); } @@ -2009,10 +2010,18 @@ private static TimeSpan GetUtcOffset(TimeSpan baseUtcOffset, AdjustmentRule adju /// /// Helper function that performs adjustment rule validation /// - private static bool IsValidAdjustmentRuleOffest(TimeSpan baseUtcOffset, AdjustmentRule adjustmentRule) + private static bool IsValidAdjustmentRuleOffset(TimeSpan baseUtcOffset, AdjustmentRule adjustmentRule) { TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); return !UtcOffsetOutOfRange(utcOffset); } + + // Helper function to create the static UTC time zone instance + private static TimeZoneInfo CreateUtcTimeZone() + { + string standardDisplayName = GetUtcStandardDisplayName(); + string displayName = $"(UTC) {standardDisplayName}"; + return CreateCustomTimeZone(UtcId, TimeSpan.Zero, displayName, standardDisplayName); + } } } diff --git a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs index a7085a9dfc41d..155346f681de8 100644 --- a/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs +++ b/src/libraries/System.Runtime.Serialization.Formatters/tests/BinaryFormatterTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -96,6 +97,17 @@ private static void ValidateAndRoundtrip(object obj, TypeSerializableValue[] blo CheckObjectTypeIntegrity(customSerializableObj); } + // TimeZoneInfo objects have three properties (DisplayName, StandardName, DaylightName) + // that are localized. Since the blobs were generated from the invariant culture, they + // will have English strings embedded. Thus, we can only test them against English + // language cultures or the invariant culture. + if (obj is TimeZoneInfo && ( + CultureInfo.CurrentUICulture.TwoLetterISOLanguageName != "en" || + CultureInfo.CurrentUICulture.Name.Length != 0)) + { + return; + } + SanityCheckBlob(obj, blobs); // ReflectionTypeLoadException and LicenseException aren't deserializable from Desktop --> Core. diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index 35861477d2388..c6c9beade9829 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -81,45 +81,62 @@ public static void Names() Assert.NotNull(utc.ToString()); } - // Due to ICU size limitations, full daylight/standard names are not included. - // name abbreviations, if available, are used instead + // Due to ICU size limitations, full daylight/standard names are not included for the browser. + // Name abbreviations, if available, are used instead public static IEnumerable Platform_TimeZoneNamesTestData() { if (PlatformDetection.IsBrowser) return new TheoryData { - { TimeZoneInfo.FindSystemTimeZoneById(s_strPacific), "(UTC-08:00) PST", "PST", "PDT" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strSydney), "(UTC+10:00) AEST", "AEST", "AEDT" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strPerth), "(UTC+08:00) AWST", "AWST", "AWDT" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strIran), "(UTC+03:30) +0330", "+0330", "+0430" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strPacific), "(UTC-08:00) America/Los_Angeles", "PST", "PDT" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strSydney), "(UTC+10:00) Australia/Sydney", "AEST", "AEDT" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strPerth), "(UTC+08:00) Australia/Perth", "AWST", "AWDT" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strIran), "(UTC+03:30) Asia/Tehran", "+0330", "+0430" }, - { s_NewfoundlandTz, "(UTC-03:30) NST", "NST", "NDT" }, - { s_catamarcaTz, "(UTC-03:00) -03", "-03", "-02" } + { s_NewfoundlandTz, "(UTC-03:30) America/St_Johns", "NST", "NDT" }, + { s_catamarcaTz, "(UTC-03:00) America/Argentina/Catamarca", "-03", "-02" } + }; + else if (PlatformDetection.IsWindows) + return new TheoryData + { + { TimeZoneInfo.FindSystemTimeZoneById(s_strPacific), "(UTC-08:00) Pacific Time (US & Canada)", "Pacific Standard Time", "Pacific Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strSydney), "(UTC+10:00) Canberra, Melbourne, Sydney", "AUS Eastern Standard Time", "AUS Eastern Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strPerth), "(UTC+08:00) Perth", "W. Australia Standard Time", "W. Australia Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strIran), "(UTC+03:30) Tehran", "Iran Standard Time", "Iran Daylight Time" }, + + { s_NewfoundlandTz, "(UTC-03:30) Newfoundland", "Newfoundland Standard Time", "Newfoundland Daylight Time" }, + { s_catamarcaTz, "(UTC-03:00) City of Buenos Aires", "Argentina Standard Time", "Argentina Daylight Time" } }; else return new TheoryData { - { TimeZoneInfo.FindSystemTimeZoneById(s_strPacific), "(UTC-08:00) Pacific Standard Time", "Pacific Standard Time", "Pacific Daylight Time" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strSydney), "(UTC+10:00) Australian Eastern Standard Time", "Australian Eastern Standard Time", "Australian Eastern Daylight Time" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strPerth), "(UTC+08:00) Australian Western Standard Time", "Australian Western Standard Time", "Australian Western Daylight Time" }, - { TimeZoneInfo.FindSystemTimeZoneById(s_strIran), "(UTC+03:30) +0330", "+0330", "+0430" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strPacific), "(UTC-08:00) Pacific Time (Los Angeles)", "Pacific Standard Time", "Pacific Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strSydney), "(UTC+10:00) Eastern Australia Time (Sydney)", "Australian Eastern Standard Time", "Australian Eastern Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strPerth), "(UTC+08:00) Australian Western Standard Time (Perth)", "Australian Western Standard Time", "Australian Western Daylight Time" }, + { TimeZoneInfo.FindSystemTimeZoneById(s_strIran), "(UTC+03:30) Iran Time", "Iran Standard Time", "Iran Daylight Time" }, - { s_NewfoundlandTz, "(UTC-03:30) NST", "NST", "NDT" }, - { s_catamarcaTz, "(UTC-03:00) -03", "-03", "-02" } + { s_NewfoundlandTz, "(UTC-03:30) Newfoundland Time (St. John’s)", "Newfoundland Standard Time", "Newfoundland Daylight Time" }, + { s_catamarcaTz, "(UTC-03:00) Argentina Standard Time (Catamarca)", "Argentina Standard Time", "Argentina Summer Time" } }; } - [Theory] + // We test the existence of a specific English time zone name to avoid failures on non-English platforms. + [ConditionalTheory(nameof(IsEnglishUILanguage))] [MemberData(nameof(Platform_TimeZoneNamesTestData))] - [PlatformSpecific(TestPlatforms.AnyUnix)] public static void Platform_TimeZoneNames(TimeZoneInfo tzi, string displayName, string standardName, string daylightName) { - if (PlatformDetection.IsBrowser) + // Edge case - Optionally allow some characters to be absent in the display name. + const string chars = ".’"; + foreach (char c in chars) { - // Console.WriteLine($"DisplayName: {tzi.DisplayName}, StandardName: {tzi.StandardName}, DaylightName: {tzi.DaylightName}"); - Assert.Equal($"DisplayName: {tzi.DisplayName}, StandardName: {tzi.StandardName}, DaylightName: {tzi.DaylightName}", - $"DisplayName: {displayName}, StandardName: {standardName}, DaylightName: {daylightName}"); + if (displayName.Contains(c, StringComparison.Ordinal) && !tzi.DisplayName.Contains(c, StringComparison.Ordinal)) + { + displayName = displayName.Replace(c.ToString(), "", StringComparison.Ordinal); + } } + + Assert.Equal($"DisplayName: \"{displayName}\", StandardName: {standardName}\", DaylightName: {daylightName}\"", + $"DisplayName: \"{tzi.DisplayName}\", StandardName: {tzi.StandardName}\", DaylightName: {tzi.DaylightName}\""); } [Fact] @@ -160,8 +177,9 @@ public static void LibyaTimeZone() Assert.True(libyaLocalTime.Equals(expectResult), string.Format("Expected {0} and got {1}", expectResult, libyaLocalTime)); } - [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows))] - public static void TestYukunTZ() + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public static void TestYukonTZ() { try { @@ -190,7 +208,7 @@ public static void TestYukunTZ() } catch (TimeZoneNotFoundException) { - // Some Windows versions don't carry the complete TZ data. Ignore the tests on such versiosn. + // Some Windows versions don't carry the complete TZ data. Ignore the tests on such versions. } } @@ -2288,6 +2306,65 @@ public static IEnumerable SystemTimeZonesTestData() { yield return new object[] { tz }; } + + // Include fixed offset IANA zones in the test data when they are available. + if (!PlatformDetection.IsWindows) + { + for (int i = -14; i <= 12; i++) + { + TimeZoneInfo tz = null; + + try + { + string id = $"Etc/GMT{i:+0;-0}"; + tz = TimeZoneInfo.FindSystemTimeZoneById(id); + } + catch (TimeZoneNotFoundException) + { + } + + if (tz != null) + { + yield return new object[] { tz }; + } + } + } + } + + [Theory] + [MemberData(nameof(SystemTimeZonesTestData))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) + { + if (timeZone.Id == TimeZoneInfo.Utc.Id || timeZone.StandardName == TimeZoneInfo.Utc.StandardName) + { + // UTC's display name is always the string "(UTC) " and the same text as the standard name. + Assert.Equal($"(UTC) {timeZone.StandardName}", timeZone.DisplayName); + + // All aliases of UTC should have the same names as UTC itself + Assert.Equal(TimeZoneInfo.Utc.DisplayName, timeZone.DisplayName); + Assert.Equal(TimeZoneInfo.Utc.StandardName, timeZone.StandardName); + Assert.Equal(TimeZoneInfo.Utc.DaylightName, timeZone.DaylightName); + } + else if (PlatformDetection.IsBrowser) + { + // Browser platform doesn't have full ICU names, but uses the IANA data instead. + + // The display name will be the offset plus the ID. + // The offset is checked separately in TimeZoneInfo_DisplayNameStartsWithOffset + Assert.EndsWith(" " + timeZone.Id, timeZone.DisplayName); + + // Match any valid IANA time zone abbreviation, including numeric forms + Assert.Matches(@"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$", timeZone.StandardName); + Assert.Matches(@"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$", timeZone.DaylightName); + } + else + { + // All we can really say generically here is that they aren't empty. + Assert.NotEmpty(timeZone.DisplayName); + Assert.NotEmpty(timeZone.StandardName); + Assert.NotEmpty(timeZone.DaylightName); + } } [ActiveIssue("https://github.com/dotnet/runtime/issues/19794", TestPlatforms.AnyUnix)] @@ -2365,44 +2442,47 @@ public static void TimeZoneInfo_DaylightDeltaIsNoMoreThan12Hours() } } - [Fact] - public static void TimeZoneInfo_DisplayNameStartsWithOffset() + [Theory] + [MemberData(nameof(SystemTimeZonesTestData))] + public static void TimeZoneInfo_DisplayNameStartsWithOffset(TimeZoneInfo tzi) { - foreach (TimeZoneInfo tzi in TimeZoneInfo.GetSystemTimeZones()) + if (tzi.StandardName == TimeZoneInfo.Utc.StandardName) { - if (tzi.Id != "UTC") - { - Assert.False(string.IsNullOrWhiteSpace(tzi.StandardName)); - Assert.Matches(@"^\(UTC(\+|-)[0-9]{2}:[0-9]{2}\) \S.*", tzi.DisplayName); + // UTC and all of its aliases (Etc/UTC, and others) start with just "(UTC) " + Assert.StartsWith("(UTC) ", tzi.DisplayName); + } + else + { + Assert.False(string.IsNullOrWhiteSpace(tzi.StandardName)); + Assert.Matches(@"^\(UTC(\+|-)[0-9]{2}:[0-9]{2}\) \S.*", tzi.DisplayName); - // see https://github.com/dotnet/corefx/pull/33204#issuecomment-438782500 - if (PlatformDetection.IsNotWindowsNanoServer && !PlatformDetection.IsWindows7) + // see https://github.com/dotnet/corefx/pull/33204#issuecomment-438782500 + if (PlatformDetection.IsNotWindowsNanoServer && !PlatformDetection.IsWindows7) + { + string offset = Regex.Match(tzi.DisplayName, @"(-|)[0-9]{2}:[0-9]{2}").Value; + TimeSpan ts = TimeSpan.Parse(offset); + if (PlatformDetection.IsWindows && + tzi.BaseUtcOffset != ts && + (tzi.Id.Contains("Morocco") || tzi.Id.Contains("Volgograd"))) { - string offset = Regex.Match(tzi.DisplayName, @"(-|)[0-9]{2}:[0-9]{2}").Value; - TimeSpan ts = TimeSpan.Parse(offset); - if (PlatformDetection.IsWindows && - tzi.BaseUtcOffset != ts && - (tzi.Id.Contains("Morocco") || tzi.Id.Contains("Volgograd"))) + // Windows data can report display name with UTC+01:00 offset which is not matching the actual BaseUtcOffset. + // We special case this in the test to avoid the test failures like: + // 01:00 != 00:00:00, dn:(UTC+01:00) Casablanca, sn:Morocco Standard Time + // 04:00 != 03:00:00, dn:(UTC+04:00) Volgograd, sn:Volgograd Standard Time + if (tzi.Id.Contains("Morocco")) { - // Windows data can report display name with UTC+01:00 offset which is not matching the actual BaseUtcOffset. - // We special case this in the test to avoid the test failures like: - // 01:00 != 00:00:00, dn:(UTC+01:00) Casablanca, sn:Morocco Standard Time - // 04:00 != 03:00:00, dn:(UTC+04:00) Volgograd, sn:Volgograd Standard Time - if (tzi.Id.Contains("Morocco")) - { - Assert.True(tzi.BaseUtcOffset == new TimeSpan(0, 0, 0), $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); - } - else - { - // Volgograd, Russia - Assert.True(tzi.BaseUtcOffset == new TimeSpan(3, 0, 0), $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); - } + Assert.True(tzi.BaseUtcOffset == new TimeSpan(0, 0, 0), $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); } else { - Assert.True(tzi.BaseUtcOffset == ts || tzi.GetUtcOffset(DateTime.Now) == ts, $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); + // Volgograd, Russia + Assert.True(tzi.BaseUtcOffset == new TimeSpan(3, 0, 0), $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); } } + else + { + Assert.True(tzi.BaseUtcOffset == ts || tzi.GetUtcOffset(DateTime.Now) == ts, $"{offset} != {tzi.BaseUtcOffset}, dn:{tzi.DisplayName}, sn:{tzi.StandardName}"); + } } } } @@ -2483,7 +2563,8 @@ public static void TestTimeZoneIdBackwardCompatibility(string oldId, string curr TimeZoneInfo currenttz = TimeZoneInfo.FindSystemTimeZoneById(currentId); Assert.Equal(oldtz.StandardName, currenttz.StandardName); - Assert.Equal(oldtz.DisplayName, currenttz.DisplayName); + Assert.Equal(oldtz.DaylightName, currenttz.DaylightName); + // Note we cannot test the DisplayName, as it will contain the ID. } [Theory] @@ -2497,7 +2578,8 @@ public static void TestTimeZoneIdBackwardCompatibility(string oldId, string curr public static void ChangeLocalTimeZone(string id) { string originalTZ = Environment.GetEnvironmentVariable("TZ"); - try { + try + { TimeZoneInfo.ClearCachedData(); Environment.SetEnvironmentVariable("TZ", id); @@ -2507,13 +2589,16 @@ public static void ChangeLocalTimeZone(string id) Assert.Equal(tz.StandardName, localtz.StandardName); Assert.Equal(tz.DisplayName, localtz.DisplayName); } - finally { + finally + { TimeZoneInfo.ClearCachedData(); Environment.SetEnvironmentVariable("TZ", originalTZ); } } - private static bool IsEnglishUILanguageAndRemoteExecutorSupported => (CultureInfo.CurrentUICulture.Name == "en" || CultureInfo.CurrentUICulture.Name.StartsWith("en-", StringComparison.Ordinal)) && RemoteExecutor.IsSupported; + private static bool IsEnglishUILanguage => CultureInfo.CurrentUICulture.Name.Length == 0 || CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "en"; + + private static bool IsEnglishUILanguageAndRemoteExecutorSupported => IsEnglishUILanguage && RemoteExecutor.IsSupported; private static void VerifyConvertException(DateTimeOffset inputTime, string destinationTimeZoneId) where TException : Exception { From 2ed3a8621100f6e0446f5af99a301723bdb55d78 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Sun, 21 Mar 2021 13:33:17 -0700 Subject: [PATCH 2/8] Improve time zone test output for clarity --- .../tests/System/TimeZoneInfoTests.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index c6c9beade9829..a78f76ec15360 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -2331,6 +2331,9 @@ public static IEnumerable SystemTimeZonesTestData() } } + private const string IanaAbbreviationPattern = @"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$"; + private static readonly Regex s_IanaAbbreviationRegex = new Regex(IanaAbbreviationPattern); + [Theory] [MemberData(nameof(SystemTimeZonesTestData))] [PlatformSpecific(TestPlatforms.AnyUnix)] @@ -2339,12 +2342,16 @@ public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) if (timeZone.Id == TimeZoneInfo.Utc.Id || timeZone.StandardName == TimeZoneInfo.Utc.StandardName) { // UTC's display name is always the string "(UTC) " and the same text as the standard name. - Assert.Equal($"(UTC) {timeZone.StandardName}", timeZone.DisplayName); + Assert.True(timeZone.DisplayName == $"(UTC) {timeZone.StandardName}", + $"Id: \"{timeZone.Id}\", Expected DisplayName: \"(UTC) {timeZone.StandardName}\", Actual DisplayName: \"{timeZone.DisplayName}\""); // All aliases of UTC should have the same names as UTC itself - Assert.Equal(TimeZoneInfo.Utc.DisplayName, timeZone.DisplayName); - Assert.Equal(TimeZoneInfo.Utc.StandardName, timeZone.StandardName); - Assert.Equal(TimeZoneInfo.Utc.DaylightName, timeZone.DaylightName); + Assert.True(timeZone.DisplayName == TimeZoneInfo.Utc.DisplayName, + $"Id: \"{timeZone.Id}\", Expected DisplayName: \"{TimeZoneInfo.Utc.DisplayName}\", Actual DisplayName: \"{timeZone.DisplayName}\""); + Assert.True(timeZone.StandardName == TimeZoneInfo.Utc.StandardName, + $"Id: \"{timeZone.Id}\", Expected StandardName: \"{TimeZoneInfo.Utc.StandardName}\", Actual StandardName: \"{timeZone.StandardName}\""); + Assert.True(timeZone.DaylightName == TimeZoneInfo.Utc.DaylightName, + $"Id: \"{timeZone.Id}\", Expected DaylightName: \"{TimeZoneInfo.Utc.DaylightName}\", Actual DaylightName: \"{timeZone.DaylightName}\""); } else if (PlatformDetection.IsBrowser) { @@ -2352,18 +2359,21 @@ public static void TimeZoneDisplayNames_Unix(TimeZoneInfo timeZone) // The display name will be the offset plus the ID. // The offset is checked separately in TimeZoneInfo_DisplayNameStartsWithOffset - Assert.EndsWith(" " + timeZone.Id, timeZone.DisplayName); + Assert.True(timeZone.DisplayName.EndsWith(" " + timeZone.Id), + $"Id: \"{timeZone.Id}\", DisplayName should have ended with the ID, Actual DisplayName: \"{timeZone.DisplayName}\""); // Match any valid IANA time zone abbreviation, including numeric forms - Assert.Matches(@"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$", timeZone.StandardName); - Assert.Matches(@"^(?:[A-Z][A-Za-z]+|[+-]\d{2}|[+-]\d{4})$", timeZone.DaylightName); + Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.StandardName), + $"Id: \"{timeZone.Id}\", StandardName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual StandardName: \"{timeZone.StandardName}\""); + Assert.True(s_IanaAbbreviationRegex.IsMatch(timeZone.DaylightName), + $"Id: \"{timeZone.Id}\", DaylightName should have matched the pattern @\"{IanaAbbreviationPattern}\", Actual DaylightName: \"{timeZone.DaylightName}\""); } else { // All we can really say generically here is that they aren't empty. - Assert.NotEmpty(timeZone.DisplayName); - Assert.NotEmpty(timeZone.StandardName); - Assert.NotEmpty(timeZone.DaylightName); + Assert.False(string.IsNullOrWhiteSpace(timeZone.DisplayName), $"Id: \"{timeZone.Id}\", DisplayName should not have been empty."); + Assert.False(string.IsNullOrWhiteSpace(timeZone.StandardName), $"Id: \"{timeZone.Id}\", StandardName should not have been empty."); + Assert.False(string.IsNullOrWhiteSpace(timeZone.DaylightName), $"Id: \"{timeZone.Id}\", DaylightName should not have been empty."); } } From f821dd07f5889fc445a316dc4d0dfcbf18dbdeeb Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Sun, 21 Mar 2021 14:12:44 -0700 Subject: [PATCH 3/8] minor improvement on substring search --- .../System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 8b70260adc7fa..63b85f09182eb 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -26,7 +26,8 @@ public sealed partial class TimeZoneInfo private const string GmtId = "GMT"; // Some time zones may give better display names using their location names rather than their generic name. - private const string ZonesThatUseLocationName = + // We can update this list as need arises. + private const string ZonesThatUseLocationName = "\n" + "Europe/Minsk\n" + // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" "Europe/Moscow\n" + // Prefer "Moscow Time" over "Moscow Standard Time" "Europe/Simferopol\n" + // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" @@ -200,7 +201,7 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan } // Also use location names in some special cases. (See the list at the top of this file.) - if (ZonesThatUseLocationName.Contains(timeZoneId, StringComparison.OrdinalIgnoreCase)) + if (ZonesThatUseLocationName.Contains($"\n{timeZoneId}\n", StringComparison.OrdinalIgnoreCase)) { displayName = $"{baseOffsetText} {genericLocationName}"; return; From fffda3c0353b122b18c5f6501cca60cce1f4cc9d Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Sun, 21 Mar 2021 14:20:28 -0700 Subject: [PATCH 4/8] Handle UTC and its aliases better --- .../src/System/TimeZoneInfo.Unix.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) 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 63b85f09182eb..1b372ff4e2b76 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -25,6 +25,19 @@ public sealed partial class TimeZoneInfo private const string FallbackCultureName = "en-US"; private const string GmtId = "GMT"; + // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml + // Hard-coded because we need to treat all aliases of UTC the same even when ICU is not available, + // or when we get "GMT" returned from older ICU versions. (This list is not likely to change.) + private const string UtcAliases = "\n" + + "Etc/UTC\n" + + "Etc/UCT\n" + + "Etc/Universal\n" + + "Etc/Zulu\n" + + "UCT\n" + + "UTC\n" + + "Universal\n" + + "Zulu\n"; + // 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 const string ZonesThatUseLocationName = "\n" + @@ -36,6 +49,19 @@ public sealed partial class TimeZoneInfo private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { + _id = id; + + // Handle UTC and its aliases + if (UtcAliases.Contains($"\n{_id}\n", StringComparison.OrdinalIgnoreCase)) + { + _standardDisplayName = GetUtcStandardDisplayName(); + _daylightDisplayName = _standardDisplayName; + _displayName = $"(UTC) {_standardDisplayName}"; + _baseUtcOffset = TimeSpan.Zero; + _adjustmentRules = Array.Empty(); + return; + } + TZifHead t; DateTime[] dts; byte[] typeOfLocalTime; @@ -50,9 +76,6 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) // parse the raw TZif bytes; this method can throw ArgumentException when the data is malformed. TZif_ParseRaw(data, out t, out dts, out typeOfLocalTime, out transitionType, out zoneAbbreviations, out StandardTime, out GmtTime, out futureTransitionsPosixFormat); - _id = id; - _baseUtcOffset = TimeSpan.Zero; - // find the best matching baseUtcOffset and display strings based on the current utcNow value. // NOTE: read the Standard and Daylight display strings from the tzfile now in case they can't be loaded later // from the globalization data. @@ -127,16 +150,8 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan // The algorithm used below should avoid duplicating the same words while still achieving the // goal of providing a unique, discoverable, and intuitive name. - string? utcStandardName = GetUtcStandardDisplayName(); - if (standardName == utcStandardName) - { - // This gives the display name for UTC and all of its aliases (Etc/UTC, Universal, etc.) - displayName = $"(UTC) {utcStandardName}"; - return; - } - // Get the base offset to prefix in front of the time zone. - // Only UTC and its aliases have "(UTC)" per above. All other zones include an offset, even if it's zero. + // 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})"; // Try to get the generic name for this time zone. From f37b4d87f7e9c7a2e8de31d63a14b6fbc2c66780 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 23 Mar 2021 11:24:54 -0700 Subject: [PATCH 5/8] Address review feedback --- .../pal_icushim_internal.h | 2 +- .../pal_timeZoneInfo.c | 316 ++++++++++-------- .../src/System/TimeZoneInfo.Unix.cs | 40 ++- 3 files changed, 211 insertions(+), 147 deletions(-) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h index 1f426969ac78d..b70e31fe9c0bb 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h @@ -59,7 +59,7 @@ // The following APIs are not supported in the ICU versions less than 52. We need to define them manually. // We have to do runtime check before using the pointers to these APIs. That is why these are listed in the FOR_ALL_OPTIONAL_ICU_FUNCTIONS list. U_CAPI int32_t U_EXPORT2 ucal_getTimeZoneIDForWindowsID(const UChar* winid, int32_t len, const char* region, UChar* id, int32_t idCapacity, UErrorCode* status); -U_CAPI int32_t U_EXPORT2 ucal_getWindowsTimeZoneID(const UChar* id, int32_t len,UChar* winid, int32_t winidCapacity, UErrorCode* status); +U_CAPI int32_t U_EXPORT2 ucal_getWindowsTimeZoneID(const UChar* id, int32_t len, UChar* winid, int32_t winidCapacity, UErrorCode* status); #endif // List of all functions from the ICU libraries that are used in the System.Globalization.Native.so 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 a6407f859eae0..c206b902cc81d 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c @@ -18,11 +18,6 @@ static const UChar GENERIC_PATTERN_UCHAR[] = {'v', 'v', 'v', 'v', '\0'}; static const UChar GENERIC_LOCATION_PATTERN_UCHAR[] = {'V', 'V', 'V', 'V', '\0'}; // u"VVVV" static const UChar EXEMPLAR_CITY_PATTERN_UCHAR[] = {'V', 'V', 'V', '\0'}; // u"VVV" -// Declare some private functions, implemented later in this file. -void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZoneId, const UDate timestamp, UCalendarDisplayNameType type, UChar* result, int32_t resultLength, UErrorCode* err); -void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZoneId, const UDate timestamp, const UChar* pattern, UChar* result, int32_t resultLength, UErrorCode* err); -void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId, const UDate timestamp, UChar* genericName, UErrorCode* err); - /* Convert Windows Time Zone Id to IANA Id */ @@ -64,66 +59,6 @@ int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* window return 0; } -/* -Gets the localized display name that is currently in effect for the specified time zone. -*/ -ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength) -{ - UErrorCode err = U_ZERO_ERROR; - char locale[ULOC_FULLNAME_CAPACITY]; - GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err); - - // Note: Due to how CLDR Metazones work, a past or future timestamp might use a different set of display names - // than are currently in effect. - // - // See https://github.com/unicode-org/cldr/blob/master/common/supplemental/metaZones.xml - // - // Example: As of writing this, Africa/Algiers is in the Europe_Central metazone, - // which has a standard-time name of "Central European Standard Time" (in English). - // However, in some previous dates, it used the Europe_Western metazone, - // having the standard-time name of "Western European Standard Time" (in English). - // Only the *current* name will be returned. - // - // TODO: Add a parameter for the timestamp that is used when getting the display names instead of - // getting "now" on the following line. Everything else should be using this timestamp. - // For now, since TimeZoneInfo presently uses only a single set of display names, we will - // use the names associated with the *current* date and time. - - UDate timestamp = ucal_getNow(); - - switch (type) - { - case TimeZoneDisplayName_Standard: - GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_STANDARD, result, resultLength, &err); - break; - - case TimeZoneDisplayName_DaylightSavings: - GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_DST, result, resultLength, &err); - break; - - case TimeZoneDisplayName_Generic: - GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_PATTERN_UCHAR, result, resultLength, &err); - if (U_SUCCESS(err)) - { - FixupTimeZoneGenericDisplayName(locale, timeZoneId, timestamp, result, &err); - } - break; - - case TimeZoneDisplayName_GenericLocation: - GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, result, resultLength, &err); - break; - - case TimeZoneDisplayName_ExemplarCity: - GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, EXEMPLAR_CITY_PATTERN_UCHAR, result, resultLength, &err); - break; - - default: - return UnknownError; - } - - return GetResultCode(err); -} - /* Private function to get the standard and daylight names from the ICU Calendar API. */ @@ -134,15 +69,16 @@ void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZo // (-1 == timeZoneId is null terminated) UCalendar* calendar = ucal_open(timeZoneId, -1, locale, UCAL_DEFAULT, err); - if (U_FAILURE(*err)) goto exit; - - ucal_setMillis(calendar, timestamp, err); - if (U_FAILURE(*err)) goto exit; - - ucal_getTimeZoneDisplayName(calendar, type, locale, result, resultLength, err); + if (U_SUCCESS(*err)) + { + ucal_setMillis(calendar, timestamp, err); + if (U_SUCCESS(*err)) + { + ucal_getTimeZoneDisplayName(calendar, type, locale, result, resultLength, err); + } - exit: - ucal_close(calendar); + ucal_close(calendar); + } } /* @@ -152,12 +88,11 @@ void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZon { // (-1 == timeZoneId and pattern are null terminated) UDateFormat* dateFormatter = udat_open(UDAT_PATTERN, UDAT_PATTERN, locale, timeZoneId, -1, pattern, -1, err); - if (U_FAILURE(*err)) goto exit; - - udat_format(dateFormatter, timestamp, result, resultLength, NULL, err); - - exit: - udat_close(dateFormatter); + if (U_SUCCESS(*err)) + { + udat_format(dateFormatter, timestamp, result, resultLength, NULL, err); + udat_close(dateFormatter); + } } /* @@ -217,84 +152,189 @@ void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId // Note that we leave the calendar open and close it later so we can also get the base offset. UChar standardName[DISPLAY_NAME_LENGTH]; UCalendar* calendar = ucal_open(timeZoneId, -1, locale, UCAL_DEFAULT, err); - if (U_FAILURE(*err)) goto exit; + if (U_FAILURE(*err)) + { + return; + } ucal_setMillis(calendar, timestamp, err); - if (U_FAILURE(*err)) goto exit; + if (U_FAILURE(*err)) + { + ucal_close(calendar); + return; + } ucal_getTimeZoneDisplayName(calendar, UCAL_STANDARD, locale, standardName, DISPLAY_NAME_LENGTH, err); - if (U_FAILURE(*err)) goto exit; + if (U_FAILURE(*err)) + { + ucal_close(calendar); + return; + } - // See if the generic name is the same as the standard name. - if (u_strcmp(genericName, standardName) == 0) + // Ensure the generic name is the same as the standard name. + if (u_strcmp(genericName, standardName) != 0) { - // Get some details for later comparison. - const int32_t originalGenericNameActualLength = u_strlen(genericName); - const int32_t baseOffset = ucal_get(calendar, UCAL_ZONE_OFFSET, err); - if (U_FAILURE(*err)) goto exit; + ucal_close(calendar); + return; + } - // Allocate some additional strings for test values. - UChar testTimeZoneId[TZID_LENGTH]; - UChar testDisplayName[DISPLAY_NAME_LENGTH]; - UChar testDisplayName2[DISPLAY_NAME_LENGTH]; + // Get some details for later comparison. + const int32_t originalGenericNameActualLength = u_strlen(genericName); + const int32_t baseOffset = ucal_get(calendar, UCAL_ZONE_OFFSET, err); + if (U_FAILURE(*err)) + { + ucal_close(calendar); + return; + } - // Enumerate over all the time zones having the same base offset. - UEnumeration* pEnum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_CANONICAL_LOCATION, NULL, &baseOffset, err); - if (U_FAILURE(*err)) goto exitEnum; + // Allocate some additional strings for test values. + UChar testTimeZoneId[TZID_LENGTH]; + UChar testDisplayName[DISPLAY_NAME_LENGTH]; + UChar testDisplayName2[DISPLAY_NAME_LENGTH]; - int count = uenum_count(pEnum, err); - if (U_FAILURE(*err)) goto exitEnum; + // Enumerate over all the time zones having the same base offset. + UEnumeration* pEnum = ucal_openTimeZoneIDEnumeration(UCAL_ZONE_TYPE_CANONICAL_LOCATION, NULL, &baseOffset, err); + if (U_FAILURE(*err)) + { + uenum_close(pEnum); + ucal_close(calendar); + return; + } - for (int i = 0; i < count; i++) + int count = uenum_count(pEnum, err); + if (U_FAILURE(*err)) + { + uenum_close(pEnum); + ucal_close(calendar); + return; + } + + for (int i = 0; i < count; i++) + { + // Get a time zone id from the enumeration to test with. + int32_t testIdLength; + const char* testId = uenum_next(pEnum, &testIdLength, err); + if (U_FAILURE(*err)) { - // Get a time zone id from the enumeration to test with. - int32_t testIdLength; - const char* testId = uenum_next(pEnum, &testIdLength, err); - if (U_FAILURE(*err)) goto exitEnum; - u_uastrcpy(testTimeZoneId, testId); - - // Get the standard name from the test time zone. - GetTimeZoneDisplayName_FromCalendar(locale, testTimeZoneId, timestamp, UCAL_STANDARD, testDisplayName, DISPLAY_NAME_LENGTH, err); - if (U_FAILURE(*err)) goto exitEnum; - - // See if the test time zone has a different standard name. - if (u_strcmp(testDisplayName, standardName) != 0) - { - // It has a different standard name. We can't use it. - continue; - } + // There shouldn't be a failure in enumeration, but if there was then exit. + uenum_close(pEnum); + ucal_close(calendar); + return; + } - // Get the generic name from the test time zone. - GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_PATTERN_UCHAR, testDisplayName, DISPLAY_NAME_LENGTH, err); - if (U_FAILURE(*err)) goto exitEnum; + // Make a UChar[] version of the test time zone id for use in the API calls. + u_uastrcpy(testTimeZoneId, testId); - // See if the test time zone has a longer (or same size) generic name. - if (u_strlen(testDisplayName) >= originalGenericNameActualLength) - { - // The test time zone's generic name isn't any shorter than the one we already have. - continue; - } + // Get the standard name from the test time zone. + GetTimeZoneDisplayName_FromCalendar(locale, testTimeZoneId, timestamp, UCAL_STANDARD, testDisplayName, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) + { + // Failed, but keep trying through the rest of the loop in case the failure is specific to this test zone. + continue; + } - // We probably have found a better generic name. But just to be safe, make sure the test zone isn't - // using a generic name that is specific to a particular location. For example, "Antarctica/Troll" - // uses "Troll Time" as a generic name, but "Greenwich Mean Time" as a standard name. We don't - // want other zones that use "Greenwich Mean Time" to be labeled as "Troll Time". + // See if the test time zone has a different standard name. + if (u_strcmp(testDisplayName, standardName) != 0) + { + // It has a different standard name. We can't use it. + continue; + } - GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, testDisplayName2, DISPLAY_NAME_LENGTH, err); - if (U_FAILURE(*err)) goto exitEnum; + // Get the generic name from the test time zone. + GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_PATTERN_UCHAR, testDisplayName, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) + { + // Failed, but keep trying through the rest of the loop in case the failure is specific to this test zone. + continue; + } - if (u_strcmp(testDisplayName, testDisplayName2) != 0) - { - // We have found a better generic name. Use it. - u_strcpy(genericName, testDisplayName); - break; - } + // See if the test time zone has a longer (or same size) generic name. + if (u_strlen(testDisplayName) >= originalGenericNameActualLength) + { + // The test time zone's generic name isn't any shorter than the one we already have. + continue; } - exitEnum: - uenum_close(pEnum); + // We probably have found a better generic name. But just to be safe, make sure the test zone isn't + // using a generic name that is specific to a particular location. For example, "Antarctica/Troll" + // uses "Troll Time" as a generic name, but "Greenwich Mean Time" as a standard name. We don't + // want other zones that use "Greenwich Mean Time" to be labeled as "Troll Time". + + GetTimeZoneDisplayName_FromPattern(locale, testTimeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, testDisplayName2, DISPLAY_NAME_LENGTH, err); + if (U_FAILURE(*err)) + { + // Failed, but keep trying through the rest of the loop in case the failure is specific to this test zone. + continue; + } + + if (u_strcmp(testDisplayName, testDisplayName2) != 0) + { + // We have found a better generic name. Use it. + u_strcpy(genericName, testDisplayName); + break; + } } - exit: + uenum_close(pEnum); ucal_close(calendar); } + +/* +Gets the localized display name that is currently in effect for the specified time zone. +*/ +ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, const UChar* timeZoneId, TimeZoneDisplayNameType type, UChar* result, int32_t resultLength) +{ + UErrorCode err = U_ZERO_ERROR; + char locale[ULOC_FULLNAME_CAPACITY]; + GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err); + + // Note: Due to how CLDR Metazones work, a past or future timestamp might use a different set of display names + // than are currently in effect. + // + // See https://github.com/unicode-org/cldr/blob/master/common/supplemental/metaZones.xml + // + // Example: As of writing this, Africa/Algiers is in the Europe_Central metazone, + // which has a standard-time name of "Central European Standard Time" (in English). + // However, in some previous dates, it used the Europe_Western metazone, + // having the standard-time name of "Western European Standard Time" (in English). + // Only the *current* name will be returned. + // + // TODO: Add a parameter for the timestamp that is used when getting the display names instead of + // getting "now" on the following line. Everything else should be using this timestamp. + // For now, since TimeZoneInfo presently uses only a single set of display names, we will + // use the names associated with the *current* date and time. + + UDate timestamp = ucal_getNow(); + + switch (type) + { + case TimeZoneDisplayName_Standard: + GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_STANDARD, result, resultLength, &err); + break; + + case TimeZoneDisplayName_DaylightSavings: + GetTimeZoneDisplayName_FromCalendar(locale, timeZoneId, timestamp, UCAL_DST, result, resultLength, &err); + break; + + case TimeZoneDisplayName_Generic: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_PATTERN_UCHAR, result, resultLength, &err); + if (U_SUCCESS(err)) + { + FixupTimeZoneGenericDisplayName(locale, timeZoneId, timestamp, result, &err); + } + break; + + case TimeZoneDisplayName_GenericLocation: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, GENERIC_LOCATION_PATTERN_UCHAR, result, resultLength, &err); + break; + + case TimeZoneDisplayName_ExemplarCity: + GetTimeZoneDisplayName_FromPattern(locale, timeZoneId, timestamp, EXEMPLAR_CITY_PATTERN_UCHAR, result, resultLength, &err); + break; + + default: + return UnknownError; + } + + return GetResultCode(err); +} \ No newline at end of file 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 1b372ff4e2b76..32c692889b6a9 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -52,7 +52,7 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) _id = id; // Handle UTC and its aliases - if (UtcAliases.Contains($"\n{_id}\n", StringComparison.OrdinalIgnoreCase)) + if (DelimitedStringContains(_id, UtcAliases, '\n', StringComparison.OrdinalIgnoreCase)) { _standardDisplayName = GetUtcStandardDisplayName(); _daylightDisplayName = _standardDisplayName; @@ -125,7 +125,7 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) // Attempt to populate the fields backing the StandardName, DaylightName, and DisplayName from globalization data. GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.Standard, uiCulture.Name, ref _standardDisplayName); GetDisplayName(_id, Interop.Globalization.TimeZoneDisplayNameType.DaylightSavings, uiCulture.Name, ref _daylightDisplayName); - GetFullValueForDisplayNameField(_id, _baseUtcOffset, _standardDisplayName, uiCulture, ref _displayName); + GetFullValueForDisplayNameField(_id, _baseUtcOffset, uiCulture, ref _displayName); // TZif supports seconds-level granularity with offsets but TimeZoneInfo only supports minutes since it aligns // with DateTimeOffset, SQL Server, and the W3C XML Specification @@ -144,7 +144,7 @@ private TimeZoneInfo(byte[] data, string id, bool dstDisabled) } // Helper function that builds the value backing the DisplayName field from gloablization data. - private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan baseUtcOffset, string? standardName, CultureInfo uiCulture, ref string? displayName) + 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 @@ -156,10 +156,7 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan // Try to get the generic name for this time zone. string? genericName = null; - if (!GlobalizationMode.Invariant) - { - GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); - } + GetDisplayName(timeZoneId, Interop.Globalization.TimeZoneDisplayNameType.Generic, uiCulture.Name, ref genericName); if (genericName == null) { @@ -216,7 +213,7 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan } // Also use location names in some special cases. (See the list at the top of this file.) - if (ZonesThatUseLocationName.Contains($"\n{timeZoneId}\n", StringComparison.OrdinalIgnoreCase)) + if (DelimitedStringContains(timeZoneId, ZonesThatUseLocationName, '\n', StringComparison.OrdinalIgnoreCase)) { displayName = $"{baseOffsetText} {genericLocationName}"; return; @@ -1967,5 +1964,32 @@ private static string GetUtcStandardDisplayName() return standardDisplayName; } + + // Optimized string contains function to ensure exact match without allocating extra strings or arrays. + // Each distinct substring within the source string must be separated by the delimiter. + // The source must also start and end with the delimiter. + private static bool DelimitedStringContains(string pattern, string source, char delimiter, StringComparison comparison) + { + Debug.Assert(source.Length > 1); + + int index = 1; + do + { + index = source.IndexOf(pattern, index, comparison); + if (index < 0) + { + return false; + } + + if (index + pattern.Length < source.Length && source[index - 1] == delimiter && source[index + pattern.Length] == delimiter) + { + return true; + } + + index += pattern.Length; + } while (index < source.Length); + + return false; + } } } From 50d0fd26ec1887994fee182e0c625963bf94e996 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 23 Mar 2021 12:13:07 -0700 Subject: [PATCH 6/8] Fix build issues --- .../Unix/System.Globalization.Native/pal_timeZoneInfo.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 c206b902cc81d..358d708232933 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c @@ -62,7 +62,7 @@ int32_t GlobalizationNative_IanaIdToWindowsId(const UChar* ianaId, UChar* window /* Private function to get the standard and daylight names from the ICU Calendar API. */ -void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZoneId, const UDate timestamp, UCalendarDisplayNameType type, UChar* result, int32_t resultLength, UErrorCode* err) +static void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZoneId, const UDate timestamp, UCalendarDisplayNameType type, UChar* result, int32_t resultLength, UErrorCode* err) { // Examples: "Pacific Standard Time" (standard) // "Pacific Daylight Time" (daylight) @@ -84,7 +84,7 @@ void GetTimeZoneDisplayName_FromCalendar(const char* locale, const UChar* timeZo /* Private function to get the various forms of generic time zone names using patterns with the ICU Date Formatting API. */ -void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZoneId, const UDate timestamp, const UChar* pattern, UChar* result, int32_t resultLength, UErrorCode* err) +static void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZoneId, const UDate timestamp, const UChar* pattern, UChar* result, int32_t resultLength, UErrorCode* err) { // (-1 == timeZoneId and pattern are null terminated) UDateFormat* dateFormatter = udat_open(UDAT_PATTERN, UDAT_PATTERN, locale, timeZoneId, -1, pattern, -1, err); @@ -98,7 +98,7 @@ void GetTimeZoneDisplayName_FromPattern(const char* locale, const UChar* timeZon /* Private function to modify the generic display name to better suit our needs. */ -void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId, const UDate timestamp, UChar* genericName, UErrorCode* err) +static void FixupTimeZoneGenericDisplayName(const char* locale, const UChar* timeZoneId, const UDate timestamp, UChar* genericName, UErrorCode* err) { // By default, some time zones will still give a standard name instead of the generic // non-location name. @@ -337,4 +337,4 @@ ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, c } return GetResultCode(err); -} \ No newline at end of file +} From 28465731f6c28251010ba8517436d20ba0e5a87b Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 23 Mar 2021 13:25:17 -0700 Subject: [PATCH 7/8] Check err from GetLocale --- .../Unix/System.Globalization.Native/pal_timeZoneInfo.c | 4 ++++ 1 file changed, 4 insertions(+) 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 358d708232933..9173931716c0e 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_timeZoneInfo.c @@ -287,6 +287,10 @@ ResultCode GlobalizationNative_GetTimeZoneDisplayName(const UChar* localeName, c UErrorCode err = U_ZERO_ERROR; char locale[ULOC_FULLNAME_CAPACITY]; GetLocale(localeName, locale, ULOC_FULLNAME_CAPACITY, false, &err); + if (U_FAILURE(err)) + { + return GetResultCode(err); + } // Note: Due to how CLDR Metazones work, a past or future timestamp might use a different set of display names // than are currently in effect. From b79ff100f570dda371c173e7e0c5b23e51d9833e Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 23 Mar 2021 13:40:44 -0700 Subject: [PATCH 8/8] Switch back to using arrays --- .../src/System/TimeZoneInfo.Unix.cs | 79 ++++++++----------- 1 file changed, 34 insertions(+), 45 deletions(-) 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 32c692889b6a9..3a5899c6c2c24 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -28,31 +28,33 @@ public sealed partial class TimeZoneInfo // UTC aliases per https://github.com/unicode-org/cldr/blob/master/common/bcp47/timezone.xml // Hard-coded because we need to treat all aliases of UTC the same even when ICU is not available, // or when we get "GMT" returned from older ICU versions. (This list is not likely to change.) - private const string UtcAliases = "\n" + - "Etc/UTC\n" + - "Etc/UCT\n" + - "Etc/Universal\n" + - "Etc/Zulu\n" + - "UCT\n" + - "UTC\n" + - "Universal\n" + - "Zulu\n"; + private static readonly string[] s_UtcAliases = new[] { + "Etc/UTC", + "Etc/UCT", + "Etc/Universal", + "Etc/Zulu", + "UCT", + "UTC", + "Universal", + "Zulu" + }; // 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 const string ZonesThatUseLocationName = "\n" + - "Europe/Minsk\n" + // Prefer "Belarus Time" over "Moscow Standard Time (Minsk)" - "Europe/Moscow\n" + // Prefer "Moscow Time" over "Moscow Standard Time" - "Europe/Simferopol\n" + // Prefer "Simferopol Time" over "Moscow Standard Time (Simferopol)" - "Pacific/Apia\n" + // Prefer "Samoa Time" over "Apia Time" - "Pacific/Pitcairn\n"; // Prefer "Pitcairn Islands Time" over "Pitcairn Time" + 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" + }; private TimeZoneInfo(byte[] data, string id, bool dstDisabled) { _id = id; // Handle UTC and its aliases - if (DelimitedStringContains(_id, UtcAliases, '\n', StringComparison.OrdinalIgnoreCase)) + if (StringArrayContains(_id, s_UtcAliases, StringComparison.OrdinalIgnoreCase)) { _standardDisplayName = GetUtcStandardDisplayName(); _daylightDisplayName = _standardDisplayName; @@ -212,8 +214,8 @@ private static void GetFullValueForDisplayNameField(string timeZoneId, TimeSpan return; } - // Also use location names in some special cases. (See the list at the top of this file.) - if (DelimitedStringContains(timeZoneId, ZonesThatUseLocationName, '\n', StringComparison.OrdinalIgnoreCase)) + // Prefer location names in some special cases. + if (StringArrayContains(timeZoneId, s_ZonesThatUseLocationName, StringComparison.OrdinalIgnoreCase)) { displayName = $"{baseOffsetText} {genericLocationName}"; return; @@ -1946,6 +1948,20 @@ private enum TZVersion : byte // when adding more versions, ensure all the logic using TZVersion is still correct } + // Helper function for string array search. (LINQ is not available here.) + private static bool StringArrayContains(string value, string[] source, StringComparison comparison) + { + foreach (string s in source) + { + if (string.Equals(s, value, comparison)) + { + return true; + } + } + + return false; + } + // Helper function to get the standard display name for the UTC static time zone instance private static string GetUtcStandardDisplayName() { @@ -1964,32 +1980,5 @@ private static string GetUtcStandardDisplayName() return standardDisplayName; } - - // Optimized string contains function to ensure exact match without allocating extra strings or arrays. - // Each distinct substring within the source string must be separated by the delimiter. - // The source must also start and end with the delimiter. - private static bool DelimitedStringContains(string pattern, string source, char delimiter, StringComparison comparison) - { - Debug.Assert(source.Length > 1); - - int index = 1; - do - { - index = source.IndexOf(pattern, index, comparison); - if (index < 0) - { - return false; - } - - if (index + pattern.Length < source.Length && source[index - 1] == delimiter && source[index + pattern.Length] == delimiter) - { - return true; - } - - index += pattern.Length; - } while (index < source.Length); - - return false; - } } }