From 12677519ad267cf6bb5b28c8f8a8d39268b6e8de Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Sun, 11 Jun 2023 21:29:11 -0700 Subject: [PATCH] Polyfill: Refactor time zone identifier handling --- polyfill/lib/ecmascript.mjs | 100 +- polyfill/lib/intl.mjs | 38 +- polyfill/lib/timezone.mjs | 5 +- polyfill/lib/zoneddatetime.mjs | 12 +- polyfill/test/cldr-timezone.json | 1927 ++++++++++++++++++++++++++++++ polyfill/test/ecmascript.mjs | 56 + 6 files changed, 2113 insertions(+), 25 deletions(-) create mode 100644 polyfill/test/cldr-timezone.json diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 4777893822..eb2859de2a 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -4,6 +4,7 @@ const ArrayIncludes = Array.prototype.includes; const ArrayPrototypePush = Array.prototype.push; const ArrayPrototypeSort = Array.prototype.sort; const IntlDateTimeFormat = globalThis.Intl.DateTimeFormat; +const IntlSupportedValuesOf = globalThis.Intl.supportedValuesOf; const MathAbs = Math.abs; const MathFloor = Math.floor; const MathMax = Math.max; @@ -368,7 +369,9 @@ export function ParseTemporalTimeZone(stringIdent) { const { tzName, offset, z } = ParseTemporalTimeZoneString(stringIdent); if (tzName) { if (IsTimeZoneOffsetString(tzName)) return CanonicalizeTimeZoneOffsetString(tzName); - return GetCanonicalTimeZoneIdentifier(tzName); + const record = GetAvailableNamedTimeZoneIdentifier(tzName); + if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); + return record.primaryIdentifier; } if (z) return 'UTC'; // if !tzName && !z then offset must be present @@ -2589,15 +2592,92 @@ export function ParseTimeZoneOffsetString(string) { return sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds); } -// In the spec, GetCanonicalTimeZoneIdentifier is infallible and is always -// preceded by a call to IsAvailableTimeZoneName. However in the polyfill, -// we don't (yet) have a way to check if a time zone ID is valid without -// also canonicalizing it. So we combine both operations into one function, -// which will return the canonical ID if the ID is valid, and will throw -// if it's not. -export function GetCanonicalTimeZoneIdentifier(timeZoneIdentifier) { - const formatter = getIntlDateTimeFormatEnUsForTimeZone(timeZoneIdentifier); - return formatter.resolvedOptions().timeZone; +let canonicalTimeZoneIdsCache = undefined; + +export function GetAvailableNamedTimeZoneIdentifier(identifier) { + // TODO: should there be an assertion here that IsTimeZoneOffsetString returns false? + if (IsTimeZoneOffsetString(identifier)) return undefined; + + // The most common case is when the identifier is a canonical time zone ID. + // Fast-path that case by caching a list of canonical IDs. If this is an old + // ECMAScript implementation that doesn't have this API, we'll set the cache + // to `null` so we won't bother trying again. + if (canonicalTimeZoneIdsCache === undefined) { + const canonicalTimeZoneIds = IntlSupportedValuesOf?.('timeZone') ?? null; + if (canonicalTimeZoneIds) { + const entries = canonicalTimeZoneIds.map((id) => [ASCIILowercase(id), id]); + canonicalTimeZoneIdsCache = new Map(entries); + } + } + + const lower = ASCIILowercase(identifier); + let primaryIdentifier = canonicalTimeZoneIdsCache?.get(lower); + if (primaryIdentifier) return { identifier: primaryIdentifier, primaryIdentifier }; + + // It's not already a primary identifier, so get its primary identifier (or + // return if it's not an available named time zone ID). + try { + const formatter = getIntlDateTimeFormatEnUsForTimeZone(identifier); + primaryIdentifier = formatter.resolvedOptions().timeZone; + } catch { + return undefined; + } + + // The identifier is an alias (a deprecated identifier that's a synonym for + // a primary identifier), so we need to case-normalize the identifier to + // match the IANA TZDB, e.g. america/new_york => America/New_York. There's + // no built-in way to do this using Intl.DateTimeFormat, but the we can + // normalize almost all aliases (modulo a few special cases) using the + // TZDB's basic capitalization pattern: + // 1. capitalize the first letter of the identifier + // 2. capitalize the letter after every slash, dash, or underscore delimiter + const standardCase = [...lower] + .map((c, i) => (i === 0 || '/-_'.includes(lower[i - 1]) ? c.toUpperCase() : c)) + .join(''); + const segments = standardCase.split('/'); + + if (segments.length === 1) { + // For single-segment legacy IDs, if it's 2-3 chars or contains a number or dash, then + // (except for the "GB-Eire" special case) the case-normalized form is all uppercase. + // GMT+0, GMT-0, ACT, LHI, NSW, GB, NZ, PRC, ROC, ROK, UCT, GMT, GMT0, + // CET, CST6CDT, EET, EST, HST, MET, MST, MST7MDT, PST8PDT, WET, NZ-CHAT, W-SU + // Otherwise it's the standard form: first letter capitalized, e.g. Iran, Egypt, Hongkong + if (lower === 'gb-eire') return { identifier: 'GB-Eire', primaryIdentifier }; + return { + identifier: lower.length <= 3 || /[-0-9]/.test(lower) ? lower.toUpperCase() : segments[0], + primaryIdentifier + }; + } + + // All Etc zone names are upper case except a few exceptions. + if (segments[0] === 'Etc') { + const etcName = ['Zulu', 'Greenwich', 'Universal'].includes(segments[1]) ? segments[1] : segments[1].toUpperCase(); + return { identifier: `Etc/${etcName}`, primaryIdentifier }; + } + + // Legacy US identifiers like US/Alaska or US/Indiana-Starke. They're always 2 segments and use standard case. + if (segments[0] === 'Us') return { identifier: `US/${segments[1]}`, primaryIdentifier }; + + // For multi-segment IDs, there's a few special cases in the second/third segments + const specialCases = { + Act: 'ACT', + Lhi: 'LHI', + Nsw: 'NSW', + Dar_Es_Salaam: 'Dar_es_Salaam', + Port_Of_Spain: 'Port_of_Spain', + Isle_Of_Man: 'Isle_of_Man', + Comodrivadavia: 'ComodRivadavia', + Knox_In: 'Knox_IN', + Dumontdurville: 'DumontDUrville', + Mcmurdo: 'McMurdo', + Denoronha: 'DeNoronha', + Easterisland: 'EasterIsland', + Bajanorte: 'BajaNorte', + Bajasur: 'BajaSur' + }; + segments[1] = specialCases[segments[1]] ?? segments[1]; + if (segments.length > 2) segments[2] = specialCases[segments[2]] ?? segments[2]; + return { identifier: segments.join('/'), primaryIdentifier }; } export function GetNamedTimeZoneOffsetNanoseconds(id, epochNanoseconds) { diff --git a/polyfill/lib/intl.mjs b/polyfill/lib/intl.mjs index 3312d581ee..e3c6a40369 100644 --- a/polyfill/lib/intl.mjs +++ b/polyfill/lib/intl.mjs @@ -21,7 +21,8 @@ const TIME = Symbol('time'); const DATETIME = Symbol('datetime'); const INST = Symbol('instant'); const ORIGINAL = Symbol('original'); -const TZ_RESOLVED = Symbol('timezone'); +const TZ_CANONICAL = Symbol('timezone-canonical'); +const TZ_ORIGINAL = Symbol('timezone-original'); const CAL_ID = Symbol('calendar-id'); const LOCALE = Symbol('locale'); const OPTIONS = Symbol('options'); @@ -81,7 +82,7 @@ export function DateTimeFormat(locale = undefined, options = undefined) { this[LOCALE] = ro.locale; this[ORIGINAL] = original; - this[TZ_RESOLVED] = ro.timeZone; + this[TZ_CANONICAL] = ro.timeZone; this[CAL_ID] = ro.calendar; this[DATE] = dateAmend; this[YM] = yearMonthAmend; @@ -89,6 +90,25 @@ export function DateTimeFormat(locale = undefined, options = undefined) { this[TIME] = timeAmend; this[DATETIME] = datetimeAmend; this[INST] = instantAmend; + + // Save the original time zone, for a few reasons: + // - Clearer error messages + // - More clearly follows the spec for InitializeDateTimeFormat + // - Because it follows the spec more closely, will make it easier to integrate + // support of offset strings and other potential changes like proposal-canonical-tz. + const timeZoneOption = hasOptions ? options.timeZone : undefined; + if (timeZoneOption === undefined) { + this[TZ_ORIGINAL] = ro.timeZone; + } else { + const id = ES.ToString(timeZoneOption); + if (ES.IsTimeZoneOffsetString(id)) { + // Note: https://github.com/tc39/ecma402/issues/683 will remove this + throw new RangeError('Intl.DateTimeFormat does not currently support offset time zones'); + } + const record = ES.GetAvailableNamedTimeZoneIdentifier(id); + if (!record) throw new RangeError(`Intl.DateTimeFormat formats built-in time zones, not ${id}`); + this[TZ_ORIGINAL] = record.identifier; + } } DateTimeFormat.supportedLocalesOf = function (...args) { @@ -118,7 +138,9 @@ Object.defineProperty(DateTimeFormat, 'prototype', { }); function resolvedOptions() { - return this[ORIGINAL].resolvedOptions(); + const resolved = this[ORIGINAL].resolvedOptions(); + resolved.timeZone = this[TZ_CANONICAL]; + return resolved; } function format(datetime, ...rest) { @@ -335,7 +357,7 @@ function extractOverrides(temporalObj, main) { const nanosecond = GetSlot(temporalObj, ISO_NANOSECOND); const datetime = new DateTime(1970, 1, 1, hour, minute, second, millisecond, microsecond, nanosecond, main[CAL_ID]); return { - instant: ES.GetInstantFor(main[TZ_RESOLVED], datetime, 'compatible'), + instant: ES.GetInstantFor(main[TZ_CANONICAL], datetime, 'compatible'), formatter: getPropLazy(main, TIME) }; } @@ -352,7 +374,7 @@ function extractOverrides(temporalObj, main) { } const datetime = new DateTime(isoYear, isoMonth, referenceISODay, 12, 0, 0, 0, 0, 0, calendar); return { - instant: ES.GetInstantFor(main[TZ_RESOLVED], datetime, 'compatible'), + instant: ES.GetInstantFor(main[TZ_CANONICAL], datetime, 'compatible'), formatter: getPropLazy(main, YM) }; } @@ -369,7 +391,7 @@ function extractOverrides(temporalObj, main) { } const datetime = new DateTime(referenceISOYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0, calendar); return { - instant: ES.GetInstantFor(main[TZ_RESOLVED], datetime, 'compatible'), + instant: ES.GetInstantFor(main[TZ_CANONICAL], datetime, 'compatible'), formatter: getPropLazy(main, MD) }; } @@ -384,7 +406,7 @@ function extractOverrides(temporalObj, main) { } const datetime = new DateTime(isoYear, isoMonth, isoDay, 12, 0, 0, 0, 0, 0, main[CAL_ID]); return { - instant: ES.GetInstantFor(main[TZ_RESOLVED], datetime, 'compatible'), + instant: ES.GetInstantFor(main[TZ_CANONICAL], datetime, 'compatible'), formatter: getPropLazy(main, DATE) }; } @@ -421,7 +443,7 @@ function extractOverrides(temporalObj, main) { ); } return { - instant: ES.GetInstantFor(main[TZ_RESOLVED], datetime, 'compatible'), + instant: ES.GetInstantFor(main[TZ_CANONICAL], datetime, 'compatible'), formatter: getPropLazy(main, DATETIME) }; } diff --git a/polyfill/lib/timezone.mjs b/polyfill/lib/timezone.mjs index 97071c83fd..9796e980f6 100644 --- a/polyfill/lib/timezone.mjs +++ b/polyfill/lib/timezone.mjs @@ -26,12 +26,13 @@ export class TimeZone { if (arguments.length < 1) { throw new RangeError('missing argument: identifier is required'); } - let stringIdentifier = ES.ToString(identifier); if (ES.IsTimeZoneOffsetString(stringIdentifier)) { stringIdentifier = ES.CanonicalizeTimeZoneOffsetString(stringIdentifier); } else { - stringIdentifier = ES.GetCanonicalTimeZoneIdentifier(stringIdentifier); + const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier); + if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`); + stringIdentifier = record.primaryIdentifier; } CreateSlots(this); SetSlot(this, TIMEZONE_ID, stringIdentifier); diff --git a/polyfill/lib/zoneddatetime.mjs b/polyfill/lib/zoneddatetime.mjs index b386343aef..1a593c7f15 100644 --- a/polyfill/lib/zoneddatetime.mjs +++ b/polyfill/lib/zoneddatetime.mjs @@ -471,13 +471,15 @@ export class ZonedDateTime { // The rest of the defaults will be filled in by formatting the Instant } - let timeZone = ES.ToTemporalTimeZoneIdentifier(GetSlot(this, TIME_ZONE)); - if (ES.IsTimeZoneOffsetString(timeZone)) { + const timeZoneIdentifier = ES.ToTemporalTimeZoneIdentifier(GetSlot(this, TIME_ZONE)); + if (ES.IsTimeZoneOffsetString(timeZoneIdentifier)) { // Note: https://github.com/tc39/ecma402/issues/683 will remove this - throw new RangeError('toLocaleString does not support offset string time zones'); + throw new RangeError('toLocaleString does not currently support offset time zones'); + } else { + const record = ES.GetAvailableNamedTimeZoneIdentifier(timeZoneIdentifier); + if (!record) throw new RangeError(`toLocaleString formats built-in time zones, not ${timeZoneIdentifier}`); + optionsCopy.timeZone = record.primaryIdentifier; } - timeZone = ES.GetCanonicalTimeZoneIdentifier(timeZone); - optionsCopy.timeZone = timeZone; const formatter = new DateTimeFormat(locales, optionsCopy); diff --git a/polyfill/test/cldr-timezone.json b/polyfill/test/cldr-timezone.json new file mode 100644 index 0000000000..80c80a785b --- /dev/null +++ b/polyfill/test/cldr-timezone.json @@ -0,0 +1,1927 @@ +{ + "version": { + "_number": "$Revision$" + }, + "keyword": { + "u": { + "tz": { + "_description": "Time zone key", + "_alias": "timezone", + "adalv": { + "_description": "Andorra", + "_alias": "Europe/Andorra" + }, + "aedxb": { + "_description": "Dubai, United Arab Emirates", + "_alias": "Asia/Dubai" + }, + "afkbl": { + "_description": "Kabul, Afghanistan", + "_alias": "Asia/Kabul" + }, + "aganu": { + "_description": "Antigua", + "_alias": "America/Antigua" + }, + "aiaxa": { + "_description": "Anguilla", + "_alias": "America/Anguilla" + }, + "altia": { + "_description": "Tirane, Albania", + "_alias": "Europe/Tirane" + }, + "amevn": { + "_description": "Yerevan, Armenia", + "_alias": "Asia/Yerevan" + }, + "ancur": { + "_description": "Curaçao", + "_alias": "America/Curacao" + }, + "aolad": { + "_description": "Luanda, Angola", + "_alias": "Africa/Luanda" + }, + "aqams": { + "_deprecated": true, + "_description": "Amundsen-Scott Station, South Pole", + "_preferred": "nzakl" + }, + "aqcas": { + "_description": "Casey Station, Bailey Peninsula", + "_alias": "Antarctica/Casey" + }, + "aqdav": { + "_description": "Davis Station, Vestfold Hills", + "_alias": "Antarctica/Davis" + }, + "aqddu": { + "_description": "Dumont d'Urville Station, Terre Adélie", + "_alias": "Antarctica/DumontDUrville" + }, + "aqmaw": { + "_description": "Mawson Station, Holme Bay", + "_alias": "Antarctica/Mawson" + }, + "aqmcm": { + "_description": "McMurdo Station, Ross Island", + "_alias": "Antarctica/McMurdo" + }, + "aqplm": { + "_description": "Palmer Station, Anvers Island", + "_alias": "Antarctica/Palmer" + }, + "aqrot": { + "_description": "Rothera Station, Adelaide Island", + "_alias": "Antarctica/Rothera" + }, + "aqsyw": { + "_description": "Syowa Station, East Ongul Island", + "_alias": "Antarctica/Syowa" + }, + "aqtrl": { + "_description": "Troll Station, Queen Maud Land", + "_alias": "Antarctica/Troll", + "_since": "26" + }, + "aqvos": { + "_description": "Vostok Station, Lake Vostok", + "_alias": "Antarctica/Vostok" + }, + "arbue": { + "_description": "Buenos Aires, Argentina", + "_alias": "America/Buenos_Aires America/Argentina/Buenos_Aires" + }, + "arcor": { + "_description": "Córdoba, Argentina", + "_alias": "America/Cordoba America/Argentina/Cordoba America/Rosario" + }, + "arctc": { + "_description": "Catamarca, Argentina", + "_alias": "America/Catamarca America/Argentina/Catamarca America/Argentina/ComodRivadavia" + }, + "arirj": { + "_description": "La Rioja, Argentina", + "_alias": "America/Argentina/La_Rioja" + }, + "arjuj": { + "_description": "Jujuy, Argentina", + "_alias": "America/Jujuy America/Argentina/Jujuy" + }, + "arluq": { + "_description": "San Luis, Argentina", + "_alias": "America/Argentina/San_Luis" + }, + "armdz": { + "_description": "Mendoza, Argentina", + "_alias": "America/Mendoza America/Argentina/Mendoza" + }, + "arrgl": { + "_description": "Río Gallegos, Argentina", + "_alias": "America/Argentina/Rio_Gallegos" + }, + "arsla": { + "_description": "Salta, Argentina", + "_alias": "America/Argentina/Salta" + }, + "artuc": { + "_description": "Tucumán, Argentina", + "_alias": "America/Argentina/Tucuman" + }, + "aruaq": { + "_description": "San Juan, Argentina", + "_alias": "America/Argentina/San_Juan" + }, + "arush": { + "_description": "Ushuaia, Argentina", + "_alias": "America/Argentina/Ushuaia" + }, + "asppg": { + "_description": "Pago Pago, American Samoa", + "_alias": "Pacific/Pago_Pago Pacific/Samoa US/Samoa" + }, + "atvie": { + "_description": "Vienna, Austria", + "_alias": "Europe/Vienna" + }, + "auadl": { + "_description": "Adelaide, Australia", + "_alias": "Australia/Adelaide Australia/South" + }, + "aubhq": { + "_description": "Broken Hill, Australia", + "_alias": "Australia/Broken_Hill Australia/Yancowinna" + }, + "aubne": { + "_description": "Brisbane, Australia", + "_alias": "Australia/Brisbane Australia/Queensland" + }, + "audrw": { + "_description": "Darwin, Australia", + "_alias": "Australia/Darwin Australia/North" + }, + "aueuc": { + "_description": "Eucla, Australia", + "_alias": "Australia/Eucla" + }, + "auhba": { + "_description": "Hobart, Australia", + "_alias": "Australia/Hobart Australia/Tasmania" + }, + "aukns": { + "_description": "Currie, Australia", + "_alias": "Australia/Currie" + }, + "auldc": { + "_description": "Lindeman Island, Australia", + "_alias": "Australia/Lindeman" + }, + "auldh": { + "_description": "Lord Howe Island, Australia", + "_alias": "Australia/Lord_Howe Australia/LHI" + }, + "aumel": { + "_description": "Melbourne, Australia", + "_alias": "Australia/Melbourne Australia/Victoria" + }, + "aumqi": { + "_description": "Macquarie Island Station, Macquarie Island", + "_alias": "Antarctica/Macquarie", + "_since": "1.8.1" + }, + "auper": { + "_description": "Perth, Australia", + "_alias": "Australia/Perth Australia/West" + }, + "ausyd": { + "_description": "Sydney, Australia", + "_alias": "Australia/Sydney Australia/ACT Australia/Canberra Australia/NSW" + }, + "awaua": { + "_description": "Aruba", + "_alias": "America/Aruba" + }, + "azbak": { + "_description": "Baku, Azerbaijan", + "_alias": "Asia/Baku" + }, + "basjj": { + "_description": "Sarajevo, Bosnia and Herzegovina", + "_alias": "Europe/Sarajevo" + }, + "bbbgi": { + "_description": "Barbados", + "_alias": "America/Barbados" + }, + "bddac": { + "_description": "Dhaka, Bangladesh", + "_alias": "Asia/Dhaka Asia/Dacca" + }, + "bebru": { + "_description": "Brussels, Belgium", + "_alias": "Europe/Brussels" + }, + "bfoua": { + "_description": "Ouagadougou, Burkina Faso", + "_alias": "Africa/Ouagadougou" + }, + "bgsof": { + "_description": "Sofia, Bulgaria", + "_alias": "Europe/Sofia" + }, + "bhbah": { + "_description": "Bahrain", + "_alias": "Asia/Bahrain" + }, + "bibjm": { + "_description": "Bujumbura, Burundi", + "_alias": "Africa/Bujumbura" + }, + "bjptn": { + "_description": "Porto-Novo, Benin", + "_alias": "Africa/Porto-Novo" + }, + "bmbda": { + "_description": "Bermuda", + "_alias": "Atlantic/Bermuda" + }, + "bnbwn": { + "_description": "Brunei", + "_alias": "Asia/Brunei" + }, + "bolpb": { + "_description": "La Paz, Bolivia", + "_alias": "America/La_Paz" + }, + "bqkra": { + "_description": "Bonaire, Sint Estatius and Saba", + "_alias": "America/Kralendijk", + "_since": "21" + }, + "braux": { + "_description": "Araguaína, Brazil", + "_alias": "America/Araguaina" + }, + "brbel": { + "_description": "Belém, Brazil", + "_alias": "America/Belem" + }, + "brbvb": { + "_description": "Boa Vista, Brazil", + "_alias": "America/Boa_Vista" + }, + "brcgb": { + "_description": "Cuiabá, Brazil", + "_alias": "America/Cuiaba" + }, + "brcgr": { + "_description": "Campo Grande, Brazil", + "_alias": "America/Campo_Grande" + }, + "brern": { + "_description": "Eirunepé, Brazil", + "_alias": "America/Eirunepe" + }, + "brfen": { + "_description": "Fernando de Noronha, Brazil", + "_alias": "America/Noronha Brazil/DeNoronha" + }, + "brfor": { + "_description": "Fortaleza, Brazil", + "_alias": "America/Fortaleza" + }, + "brmao": { + "_description": "Manaus, Brazil", + "_alias": "America/Manaus Brazil/West" + }, + "brmcz": { + "_description": "Maceió, Brazil", + "_alias": "America/Maceio" + }, + "brpvh": { + "_description": "Porto Velho, Brazil", + "_alias": "America/Porto_Velho" + }, + "brrbr": { + "_description": "Rio Branco, Brazil", + "_alias": "America/Rio_Branco America/Porto_Acre Brazil/Acre" + }, + "brrec": { + "_description": "Recife, Brazil", + "_alias": "America/Recife" + }, + "brsao": { + "_description": "São Paulo, Brazil", + "_alias": "America/Sao_Paulo Brazil/East" + }, + "brssa": { + "_description": "Bahia, Brazil", + "_alias": "America/Bahia" + }, + "brstm": { + "_description": "Santarém, Brazil", + "_alias": "America/Santarem" + }, + "bsnas": { + "_description": "Nassau, Bahamas", + "_alias": "America/Nassau" + }, + "btthi": { + "_description": "Thimphu, Bhutan", + "_alias": "Asia/Thimphu Asia/Thimbu" + }, + "bwgbe": { + "_description": "Gaborone, Botswana", + "_alias": "Africa/Gaborone" + }, + "bymsq": { + "_description": "Minsk, Belarus", + "_alias": "Europe/Minsk" + }, + "bzbze": { + "_description": "Belize", + "_alias": "America/Belize" + }, + "cacfq": { + "_description": "Creston, Canada", + "_alias": "America/Creston", + "_since": "21.0.1" + }, + "caedm": { + "_description": "Edmonton, Canada", + "_alias": "America/Edmonton Canada/Mountain" + }, + "caffs": { + "_description": "Rainy River, Canada", + "_alias": "America/Rainy_River" + }, + "cafne": { + "_description": "Fort Nelson, Canada", + "_alias": "America/Fort_Nelson", + "_since": "29" + }, + "caglb": { + "_description": "Glace Bay, Canada", + "_alias": "America/Glace_Bay" + }, + "cagoo": { + "_description": "Goose Bay, Canada", + "_alias": "America/Goose_Bay" + }, + "cahal": { + "_description": "Halifax, Canada", + "_alias": "America/Halifax Canada/Atlantic" + }, + "caiql": { + "_description": "Iqaluit, Canada", + "_alias": "America/Iqaluit" + }, + "camon": { + "_description": "Moncton, Canada", + "_alias": "America/Moncton" + }, + "camtr": { + "_deprecated": true, + "_description": "Montreal, Canada", + "_preferred": "cator" + }, + "canpg": { + "_description": "Nipigon, Canada", + "_alias": "America/Nipigon" + }, + "capnt": { + "_description": "Pangnirtung, Canada", + "_alias": "America/Pangnirtung" + }, + "careb": { + "_description": "Resolute, Canada", + "_alias": "America/Resolute" + }, + "careg": { + "_description": "Regina, Canada", + "_alias": "America/Regina Canada/East-Saskatchewan Canada/Saskatchewan" + }, + "casjf": { + "_description": "St. John's, Canada", + "_alias": "America/St_Johns Canada/Newfoundland" + }, + "cathu": { + "_description": "Thunder Bay, Canada", + "_alias": "America/Thunder_Bay" + }, + "cator": { + "_description": "Toronto, Canada", + "_alias": "America/Toronto America/Montreal Canada/Eastern" + }, + "cavan": { + "_description": "Vancouver, Canada", + "_alias": "America/Vancouver Canada/Pacific" + }, + "cawnp": { + "_description": "Winnipeg, Canada", + "_alias": "America/Winnipeg Canada/Central" + }, + "caybx": { + "_description": "Blanc-Sablon, Canada", + "_alias": "America/Blanc-Sablon" + }, + "caycb": { + "_description": "Cambridge Bay, Canada", + "_alias": "America/Cambridge_Bay" + }, + "cayda": { + "_description": "Dawson, Canada", + "_alias": "America/Dawson" + }, + "caydq": { + "_description": "Dawson Creek, Canada", + "_alias": "America/Dawson_Creek" + }, + "cayek": { + "_description": "Rankin Inlet, Canada", + "_alias": "America/Rankin_Inlet" + }, + "cayev": { + "_description": "Inuvik, Canada", + "_alias": "America/Inuvik" + }, + "cayxy": { + "_description": "Whitehorse, Canada", + "_alias": "America/Whitehorse Canada/Yukon" + }, + "cayyn": { + "_description": "Swift Current, Canada", + "_alias": "America/Swift_Current" + }, + "cayzf": { + "_description": "Yellowknife, Canada", + "_alias": "America/Yellowknife" + }, + "cayzs": { + "_description": "Atikokan, Canada", + "_alias": "America/Coral_Harbour America/Atikokan" + }, + "cccck": { + "_description": "Cocos (Keeling) Islands", + "_alias": "Indian/Cocos" + }, + "cdfbm": { + "_description": "Lubumbashi, Democratic Republic of the Congo", + "_alias": "Africa/Lubumbashi" + }, + "cdfih": { + "_description": "Kinshasa, Democratic Republic of the Congo", + "_alias": "Africa/Kinshasa" + }, + "cfbgf": { + "_description": "Bangui, Central African Republic", + "_alias": "Africa/Bangui" + }, + "cgbzv": { + "_description": "Brazzaville, Republic of the Congo", + "_alias": "Africa/Brazzaville" + }, + "chzrh": { + "_description": "Zurich, Switzerland", + "_alias": "Europe/Zurich" + }, + "ciabj": { + "_description": "Abidjan, Côte d'Ivoire", + "_alias": "Africa/Abidjan" + }, + "ckrar": { + "_description": "Rarotonga, Cook Islands", + "_alias": "Pacific/Rarotonga" + }, + "clipc": { + "_description": "Easter Island, Chile", + "_alias": "Pacific/Easter Chile/EasterIsland" + }, + "clpuq": { + "_description": "Punta Arenas, Chile", + "_alias": "America/Punta_Arenas", + "_since": "31" + }, + "clscl": { + "_description": "Santiago, Chile", + "_alias": "America/Santiago Chile/Continental" + }, + "cmdla": { + "_description": "Douala, Cameroon", + "_alias": "Africa/Douala" + }, + "cnckg": { + "_deprecated": true, + "_description": "Chongqing, China", + "_preferred": "cnsha" + }, + "cnhrb": { + "_deprecated": true, + "_description": "Harbin, China", + "_preferred": "cnsha" + }, + "cnkhg": { + "_deprecated": true, + "_description": "Kashgar, China", + "_preferred": "cnurc" + }, + "cnsha": { + "_description": "Shanghai, China", + "_alias": "Asia/Shanghai Asia/Chongqing Asia/Chungking Asia/Harbin PRC" + }, + "cnurc": { + "_description": "Ürümqi, China", + "_alias": "Asia/Urumqi Asia/Kashgar" + }, + "cobog": { + "_description": "Bogotá, Colombia", + "_alias": "America/Bogota" + }, + "crsjo": { + "_description": "Costa Rica", + "_alias": "America/Costa_Rica" + }, + "cst6cdt": { + "_description": "POSIX style time zone for US Central Time", + "_alias": "CST6CDT", + "_since": "1.8" + }, + "cuhav": { + "_description": "Havana, Cuba", + "_alias": "America/Havana Cuba" + }, + "cvrai": { + "_description": "Cape Verde", + "_alias": "Atlantic/Cape_Verde" + }, + "cxxch": { + "_description": "Christmas Island", + "_alias": "Indian/Christmas" + }, + "cyfmg": { + "_description": "Famagusta, Cyprus", + "_alias": "Asia/Famagusta", + "_since": "31" + }, + "cynic": { + "_description": "Nicosia, Cyprus", + "_alias": "Asia/Nicosia Europe/Nicosia" + }, + "czprg": { + "_description": "Prague, Czech Republic", + "_alias": "Europe/Prague" + }, + "deber": { + "_description": "Berlin, Germany", + "_alias": "Europe/Berlin" + }, + "debsngn": { + "_description": "Busingen, Germany", + "_alias": "Europe/Busingen", + "_since": "23" + }, + "djjib": { + "_description": "Djibouti", + "_alias": "Africa/Djibouti" + }, + "dkcph": { + "_description": "Copenhagen, Denmark", + "_alias": "Europe/Copenhagen" + }, + "dmdom": { + "_description": "Dominica", + "_alias": "America/Dominica" + }, + "dosdq": { + "_description": "Santo Domingo, Dominican Republic", + "_alias": "America/Santo_Domingo" + }, + "dzalg": { + "_description": "Algiers, Algeria", + "_alias": "Africa/Algiers" + }, + "ecgps": { + "_description": "Galápagos Islands, Ecuador", + "_alias": "Pacific/Galapagos" + }, + "ecgye": { + "_description": "Guayaquil, Ecuador", + "_alias": "America/Guayaquil" + }, + "eetll": { + "_description": "Tallinn, Estonia", + "_alias": "Europe/Tallinn" + }, + "egcai": { + "_description": "Cairo, Egypt", + "_alias": "Africa/Cairo Egypt" + }, + "eheai": { + "_description": "El Aaiún, Western Sahara", + "_alias": "Africa/El_Aaiun" + }, + "erasm": { + "_description": "Asmara, Eritrea", + "_alias": "Africa/Asmera Africa/Asmara" + }, + "esceu": { + "_description": "Ceuta, Spain", + "_alias": "Africa/Ceuta" + }, + "eslpa": { + "_description": "Canary Islands, Spain", + "_alias": "Atlantic/Canary" + }, + "esmad": { + "_description": "Madrid, Spain", + "_alias": "Europe/Madrid" + }, + "est5edt": { + "_description": "POSIX style time zone for US Eastern Time", + "_alias": "EST5EDT", + "_since": "1.8" + }, + "etadd": { + "_description": "Addis Ababa, Ethiopia", + "_alias": "Africa/Addis_Ababa" + }, + "fihel": { + "_description": "Helsinki, Finland", + "_alias": "Europe/Helsinki" + }, + "fimhq": { + "_description": "Mariehamn, Åland, Finland", + "_alias": "Europe/Mariehamn" + }, + "fjsuv": { + "_description": "Fiji", + "_alias": "Pacific/Fiji" + }, + "fkpsy": { + "_description": "Stanley, Falkland Islands", + "_alias": "Atlantic/Stanley" + }, + "fmksa": { + "_description": "Kosrae, Micronesia", + "_alias": "Pacific/Kosrae" + }, + "fmpni": { + "_description": "Pohnpei, Micronesia", + "_alias": "Pacific/Ponape Pacific/Pohnpei" + }, + "fmtkk": { + "_description": "Chuuk, Micronesia", + "_alias": "Pacific/Truk Pacific/Chuuk Pacific/Yap" + }, + "fotho": { + "_description": "Faroe Islands", + "_alias": "Atlantic/Faeroe Atlantic/Faroe" + }, + "frpar": { + "_description": "Paris, France", + "_alias": "Europe/Paris" + }, + "galbv": { + "_description": "Libreville, Gabon", + "_alias": "Africa/Libreville" + }, + "gaza": { + "_deprecated": true, + "_description": "Gaza Strip, Palestinian Territories", + "_preferred": "gazastrp" + }, + "gazastrp": { + "_description": "Gaza Strip, Palestinian Territories", + "_alias": "Asia/Gaza", + "_since": "40" + }, + "gblon": { + "_description": "London, United Kingdom", + "_alias": "Europe/London Europe/Belfast GB GB-Eire" + }, + "gdgnd": { + "_description": "Grenada", + "_alias": "America/Grenada" + }, + "getbs": { + "_description": "Tbilisi, Georgia", + "_alias": "Asia/Tbilisi" + }, + "gfcay": { + "_description": "Cayenne, French Guiana", + "_alias": "America/Cayenne" + }, + "gggci": { + "_description": "Guernsey", + "_alias": "Europe/Guernsey" + }, + "ghacc": { + "_description": "Accra, Ghana", + "_alias": "Africa/Accra" + }, + "gigib": { + "_description": "Gibraltar", + "_alias": "Europe/Gibraltar" + }, + "gldkshvn": { + "_description": "Danmarkshavn, Greenland", + "_alias": "America/Danmarkshavn" + }, + "glgoh": { + "_description": "Nuuk (Godthåb), Greenland", + "_alias": "America/Godthab America/Nuuk" + }, + "globy": { + "_description": "Ittoqqortoormiit (Scoresbysund), Greenland", + "_alias": "America/Scoresbysund" + }, + "glthu": { + "_description": "Qaanaaq (Thule), Greenland", + "_alias": "America/Thule" + }, + "gmbjl": { + "_description": "Banjul, Gambia", + "_alias": "Africa/Banjul" + }, + "gmt": { + "_description": "Greenwich Mean Time", + "_alias": "Etc/GMT Etc/GMT+0 Etc/GMT-0 Etc/GMT0 Etc/Greenwich GMT GMT+0 GMT-0 GMT0 Greenwich", + "_since": "31" + }, + "gncky": { + "_description": "Conakry, Guinea", + "_alias": "Africa/Conakry" + }, + "gpbbr": { + "_description": "Guadeloupe", + "_alias": "America/Guadeloupe" + }, + "gpmsb": { + "_description": "Marigot, Saint Martin", + "_alias": "America/Marigot" + }, + "gpsbh": { + "_description": "Saint Barthélemy", + "_alias": "America/St_Barthelemy" + }, + "gqssg": { + "_description": "Malabo, Equatorial Guinea", + "_alias": "Africa/Malabo" + }, + "grath": { + "_description": "Athens, Greece", + "_alias": "Europe/Athens" + }, + "gsgrv": { + "_description": "South Georgia and the South Sandwich Islands", + "_alias": "Atlantic/South_Georgia" + }, + "gtgua": { + "_description": "Guatemala", + "_alias": "America/Guatemala" + }, + "gugum": { + "_description": "Guam", + "_alias": "Pacific/Guam" + }, + "gwoxb": { + "_description": "Bissau, Guinea-Bissau", + "_alias": "Africa/Bissau" + }, + "gygeo": { + "_description": "Guyana", + "_alias": "America/Guyana" + }, + "hebron": { + "_description": "West Bank, Palestinian Territories", + "_alias": "Asia/Hebron", + "_since": "21" + }, + "hkhkg": { + "_description": "Hong Kong SAR China", + "_alias": "Asia/Hong_Kong Hongkong" + }, + "hntgu": { + "_description": "Tegucigalpa, Honduras", + "_alias": "America/Tegucigalpa" + }, + "hrzag": { + "_description": "Zagreb, Croatia", + "_alias": "Europe/Zagreb" + }, + "htpap": { + "_description": "Port-au-Prince, Haiti", + "_alias": "America/Port-au-Prince" + }, + "hubud": { + "_description": "Budapest, Hungary", + "_alias": "Europe/Budapest" + }, + "iddjj": { + "_description": "Jayapura, Indonesia", + "_alias": "Asia/Jayapura" + }, + "idjkt": { + "_description": "Jakarta, Indonesia", + "_alias": "Asia/Jakarta" + }, + "idmak": { + "_description": "Makassar, Indonesia", + "_alias": "Asia/Makassar Asia/Ujung_Pandang" + }, + "idpnk": { + "_description": "Pontianak, Indonesia", + "_alias": "Asia/Pontianak" + }, + "iedub": { + "_description": "Dublin, Ireland", + "_alias": "Europe/Dublin Eire" + }, + "imdgs": { + "_description": "Isle of Man", + "_alias": "Europe/Isle_of_Man" + }, + "inccu": { + "_description": "Kolkata, India", + "_alias": "Asia/Calcutta Asia/Kolkata" + }, + "iodga": { + "_description": "Chagos Archipelago", + "_alias": "Indian/Chagos" + }, + "iqbgw": { + "_description": "Baghdad, Iraq", + "_alias": "Asia/Baghdad" + }, + "irthr": { + "_description": "Tehran, Iran", + "_alias": "Asia/Tehran Iran" + }, + "isrey": { + "_description": "Reykjavik, Iceland", + "_alias": "Atlantic/Reykjavik Iceland" + }, + "itrom": { + "_description": "Rome, Italy", + "_alias": "Europe/Rome" + }, + "jeruslm": { + "_description": "Jerusalem", + "_alias": "Asia/Jerusalem Asia/Tel_Aviv Israel" + }, + "jesth": { + "_description": "Jersey", + "_alias": "Europe/Jersey" + }, + "jmkin": { + "_description": "Jamaica", + "_alias": "America/Jamaica Jamaica" + }, + "joamm": { + "_description": "Amman, Jordan", + "_alias": "Asia/Amman" + }, + "jptyo": { + "_description": "Tokyo, Japan", + "_alias": "Asia/Tokyo Japan" + }, + "kenbo": { + "_description": "Nairobi, Kenya", + "_alias": "Africa/Nairobi" + }, + "kgfru": { + "_description": "Bishkek, Kyrgyzstan", + "_alias": "Asia/Bishkek" + }, + "khpnh": { + "_description": "Phnom Penh, Cambodia", + "_alias": "Asia/Phnom_Penh" + }, + "kicxi": { + "_description": "Kiritimati, Kiribati", + "_alias": "Pacific/Kiritimati" + }, + "kipho": { + "_description": "Enderbury Island, Kiribati", + "_alias": "Pacific/Enderbury Pacific/Kanton" + }, + "kitrw": { + "_description": "Tarawa, Kiribati", + "_alias": "Pacific/Tarawa" + }, + "kmyva": { + "_description": "Comoros", + "_alias": "Indian/Comoro" + }, + "knbas": { + "_description": "Saint Kitts", + "_alias": "America/St_Kitts" + }, + "kpfnj": { + "_description": "Pyongyang, North Korea", + "_alias": "Asia/Pyongyang" + }, + "krsel": { + "_description": "Seoul, South Korea", + "_alias": "Asia/Seoul ROK" + }, + "kwkwi": { + "_description": "Kuwait", + "_alias": "Asia/Kuwait" + }, + "kygec": { + "_description": "Cayman Islands", + "_alias": "America/Cayman" + }, + "kzaau": { + "_description": "Aqtau, Kazakhstan", + "_alias": "Asia/Aqtau" + }, + "kzakx": { + "_description": "Aqtobe, Kazakhstan", + "_alias": "Asia/Aqtobe" + }, + "kzala": { + "_description": "Almaty, Kazakhstan", + "_alias": "Asia/Almaty" + }, + "kzguw": { + "_description": "Atyrau (Guryev), Kazakhstan", + "_alias": "Asia/Atyrau", + "_since": "31" + }, + "kzksn": { + "_description": "Qostanay (Kostanay), Kazakhstan", + "_alias": "Asia/Qostanay", + "_since": "35" + }, + "kzkzo": { + "_description": "Kyzylorda, Kazakhstan", + "_alias": "Asia/Qyzylorda" + }, + "kzura": { + "_description": "Oral, Kazakhstan", + "_alias": "Asia/Oral" + }, + "lavte": { + "_description": "Vientiane, Laos", + "_alias": "Asia/Vientiane" + }, + "lbbey": { + "_description": "Beirut, Lebanon", + "_alias": "Asia/Beirut" + }, + "lccas": { + "_description": "Saint Lucia", + "_alias": "America/St_Lucia" + }, + "livdz": { + "_description": "Vaduz, Liechtenstein", + "_alias": "Europe/Vaduz" + }, + "lkcmb": { + "_description": "Colombo, Sri Lanka", + "_alias": "Asia/Colombo" + }, + "lrmlw": { + "_description": "Monrovia, Liberia", + "_alias": "Africa/Monrovia" + }, + "lsmsu": { + "_description": "Maseru, Lesotho", + "_alias": "Africa/Maseru" + }, + "ltvno": { + "_description": "Vilnius, Lithuania", + "_alias": "Europe/Vilnius" + }, + "lulux": { + "_description": "Luxembourg", + "_alias": "Europe/Luxembourg" + }, + "lvrix": { + "_description": "Riga, Latvia", + "_alias": "Europe/Riga" + }, + "lytip": { + "_description": "Tripoli, Libya", + "_alias": "Africa/Tripoli Libya" + }, + "macas": { + "_description": "Casablanca, Morocco", + "_alias": "Africa/Casablanca" + }, + "mcmon": { + "_description": "Monaco", + "_alias": "Europe/Monaco" + }, + "mdkiv": { + "_description": "Chişinău, Moldova", + "_alias": "Europe/Chisinau Europe/Tiraspol" + }, + "metgd": { + "_description": "Podgorica, Montenegro", + "_alias": "Europe/Podgorica" + }, + "mgtnr": { + "_description": "Antananarivo, Madagascar", + "_alias": "Indian/Antananarivo" + }, + "mhkwa": { + "_description": "Kwajalein, Marshall Islands", + "_alias": "Pacific/Kwajalein Kwajalein" + }, + "mhmaj": { + "_description": "Majuro, Marshall Islands", + "_alias": "Pacific/Majuro" + }, + "mkskp": { + "_description": "Skopje, Macedonia", + "_alias": "Europe/Skopje" + }, + "mlbko": { + "_description": "Bamako, Mali", + "_alias": "Africa/Bamako Africa/Timbuktu" + }, + "mmrgn": { + "_description": "Yangon (Rangoon), Burma", + "_alias": "Asia/Rangoon Asia/Yangon" + }, + "mncoq": { + "_description": "Choibalsan, Mongolia", + "_alias": "Asia/Choibalsan" + }, + "mnhvd": { + "_description": "Khovd (Hovd), Mongolia", + "_alias": "Asia/Hovd" + }, + "mnuln": { + "_description": "Ulaanbaatar (Ulan Bator), Mongolia", + "_alias": "Asia/Ulaanbaatar Asia/Ulan_Bator" + }, + "momfm": { + "_description": "Macau SAR China", + "_alias": "Asia/Macau Asia/Macao" + }, + "mpspn": { + "_description": "Saipan, Northern Mariana Islands", + "_alias": "Pacific/Saipan" + }, + "mqfdf": { + "_description": "Martinique", + "_alias": "America/Martinique" + }, + "mrnkc": { + "_description": "Nouakchott, Mauritania", + "_alias": "Africa/Nouakchott" + }, + "msmni": { + "_description": "Montserrat", + "_alias": "America/Montserrat" + }, + "mst7mdt": { + "_description": "POSIX style time zone for US Mountain Time", + "_alias": "MST7MDT", + "_since": "1.8" + }, + "mtmla": { + "_description": "Malta", + "_alias": "Europe/Malta" + }, + "muplu": { + "_description": "Mauritius", + "_alias": "Indian/Mauritius" + }, + "mvmle": { + "_description": "Maldives", + "_alias": "Indian/Maldives" + }, + "mwblz": { + "_description": "Blantyre, Malawi", + "_alias": "Africa/Blantyre" + }, + "mxchi": { + "_description": "Chihuahua, Mexico", + "_alias": "America/Chihuahua" + }, + "mxcjs": { + "_description": "Ciudad Juárez, Mexico", + "_alias": "America/Ciudad_Juarez", + "_since": "43" + }, + "mxcun": { + "_description": "Cancún, Mexico", + "_alias": "America/Cancun" + }, + "mxhmo": { + "_description": "Hermosillo, Mexico", + "_alias": "America/Hermosillo" + }, + "mxmam": { + "_description": "Matamoros, Mexico", + "_alias": "America/Matamoros" + }, + "mxmex": { + "_description": "Mexico City, Mexico", + "_alias": "America/Mexico_City Mexico/General" + }, + "mxmid": { + "_description": "Mérida, Mexico", + "_alias": "America/Merida" + }, + "mxmty": { + "_description": "Monterrey, Mexico", + "_alias": "America/Monterrey" + }, + "mxmzt": { + "_description": "Mazatlán, Mexico", + "_alias": "America/Mazatlan Mexico/BajaSur" + }, + "mxoji": { + "_description": "Ojinaga, Mexico", + "_alias": "America/Ojinaga" + }, + "mxpvr": { + "_description": "Bahía de Banderas, Mexico", + "_alias": "America/Bahia_Banderas", + "_since": "1.9" + }, + "mxstis": { + "_description": "Santa Isabel (Baja California), Mexico", + "_alias": "America/Santa_Isabel" + }, + "mxtij": { + "_description": "Tijuana, Mexico", + "_alias": "America/Tijuana America/Ensenada Mexico/BajaNorte" + }, + "mykch": { + "_description": "Kuching, Malaysia", + "_alias": "Asia/Kuching" + }, + "mykul": { + "_description": "Kuala Lumpur, Malaysia", + "_alias": "Asia/Kuala_Lumpur" + }, + "mzmpm": { + "_description": "Maputo, Mozambique", + "_alias": "Africa/Maputo" + }, + "nawdh": { + "_description": "Windhoek, Namibia", + "_alias": "Africa/Windhoek" + }, + "ncnou": { + "_description": "Noumea, New Caledonia", + "_alias": "Pacific/Noumea" + }, + "nenim": { + "_description": "Niamey, Niger", + "_alias": "Africa/Niamey" + }, + "nfnlk": { + "_description": "Norfolk Island", + "_alias": "Pacific/Norfolk" + }, + "nglos": { + "_description": "Lagos, Nigeria", + "_alias": "Africa/Lagos" + }, + "nimga": { + "_description": "Managua, Nicaragua", + "_alias": "America/Managua" + }, + "nlams": { + "_description": "Amsterdam, Netherlands", + "_alias": "Europe/Amsterdam" + }, + "noosl": { + "_description": "Oslo, Norway", + "_alias": "Europe/Oslo" + }, + "npktm": { + "_description": "Kathmandu, Nepal", + "_alias": "Asia/Katmandu Asia/Kathmandu" + }, + "nrinu": { + "_description": "Nauru", + "_alias": "Pacific/Nauru" + }, + "nuiue": { + "_description": "Niue", + "_alias": "Pacific/Niue" + }, + "nzakl": { + "_description": "Auckland, New Zealand", + "_alias": "Pacific/Auckland Antarctica/South_Pole NZ" + }, + "nzcht": { + "_description": "Chatham Islands, New Zealand", + "_alias": "Pacific/Chatham NZ-CHAT" + }, + "ommct": { + "_description": "Muscat, Oman", + "_alias": "Asia/Muscat" + }, + "papty": { + "_description": "Panama", + "_alias": "America/Panama" + }, + "pelim": { + "_description": "Lima, Peru", + "_alias": "America/Lima" + }, + "pfgmr": { + "_description": "Gambiera Islands, French Polynesia", + "_alias": "Pacific/Gambier" + }, + "pfnhv": { + "_description": "Marquesas Islands, French Polynesia", + "_alias": "Pacific/Marquesas" + }, + "pfppt": { + "_description": "Tahiti, French Polynesia", + "_alias": "Pacific/Tahiti" + }, + "pgpom": { + "_description": "Port Moresby, Papua New Guinea", + "_alias": "Pacific/Port_Moresby" + }, + "pgraw": { + "_description": "Bougainville, Papua New Guinea", + "_alias": "Pacific/Bougainville", + "_since": "27" + }, + "phmnl": { + "_description": "Manila, Philippines", + "_alias": "Asia/Manila" + }, + "pkkhi": { + "_description": "Karachi, Pakistan", + "_alias": "Asia/Karachi" + }, + "plwaw": { + "_description": "Warsaw, Poland", + "_alias": "Europe/Warsaw Poland" + }, + "pmmqc": { + "_description": "Saint Pierre and Miquelon", + "_alias": "America/Miquelon" + }, + "pnpcn": { + "_description": "Pitcairn Islands", + "_alias": "Pacific/Pitcairn" + }, + "prsju": { + "_description": "Puerto Rico", + "_alias": "America/Puerto_Rico" + }, + "pst8pdt": { + "_description": "POSIX style time zone for US Pacific Time", + "_alias": "PST8PDT", + "_since": "1.8" + }, + "ptfnc": { + "_description": "Madeira, Portugal", + "_alias": "Atlantic/Madeira" + }, + "ptlis": { + "_description": "Lisbon, Portugal", + "_alias": "Europe/Lisbon Portugal" + }, + "ptpdl": { + "_description": "Azores, Portugal", + "_alias": "Atlantic/Azores" + }, + "pwror": { + "_description": "Palau", + "_alias": "Pacific/Palau" + }, + "pyasu": { + "_description": "Asunción, Paraguay", + "_alias": "America/Asuncion" + }, + "qadoh": { + "_description": "Qatar", + "_alias": "Asia/Qatar" + }, + "rereu": { + "_description": "Réunion", + "_alias": "Indian/Reunion" + }, + "robuh": { + "_description": "Bucharest, Romania", + "_alias": "Europe/Bucharest" + }, + "rsbeg": { + "_description": "Belgrade, Serbia", + "_alias": "Europe/Belgrade" + }, + "ruasf": { + "_description": "Astrakhan, Russia", + "_alias": "Europe/Astrakhan", + "_since": "30" + }, + "rubax": { + "_description": "Barnaul, Russia", + "_alias": "Asia/Barnaul", + "_since": "30" + }, + "ruchita": { + "_description": "Chita Zabaykalsky, Russia", + "_alias": "Asia/Chita", + "_since": "26" + }, + "rudyr": { + "_description": "Anadyr, Russia", + "_alias": "Asia/Anadyr" + }, + "rugdx": { + "_description": "Magadan, Russia", + "_alias": "Asia/Magadan" + }, + "ruikt": { + "_description": "Irkutsk, Russia", + "_alias": "Asia/Irkutsk" + }, + "rukgd": { + "_description": "Kaliningrad, Russia", + "_alias": "Europe/Kaliningrad" + }, + "rukhndg": { + "_description": "Khandyga Tomponsky, Russia", + "_alias": "Asia/Khandyga", + "_since": "23" + }, + "rukra": { + "_description": "Krasnoyarsk, Russia", + "_alias": "Asia/Krasnoyarsk" + }, + "rukuf": { + "_description": "Samara, Russia", + "_alias": "Europe/Samara" + }, + "rukvx": { + "_description": "Kirov, Russia", + "_alias": "Europe/Kirov", + "_since": "30" + }, + "rumow": { + "_description": "Moscow, Russia", + "_alias": "Europe/Moscow W-SU" + }, + "runoz": { + "_description": "Novokuznetsk, Russia", + "_alias": "Asia/Novokuznetsk" + }, + "ruoms": { + "_description": "Omsk, Russia", + "_alias": "Asia/Omsk" + }, + "ruovb": { + "_description": "Novosibirsk, Russia", + "_alias": "Asia/Novosibirsk" + }, + "rupkc": { + "_description": "Kamchatka Peninsula, Russia", + "_alias": "Asia/Kamchatka" + }, + "rurtw": { + "_description": "Saratov, Russia", + "_alias": "Europe/Saratov", + "_since": "31" + }, + "rusred": { + "_description": "Srednekolymsk, Russia", + "_alias": "Asia/Srednekolymsk", + "_since": "26" + }, + "rutof": { + "_description": "Tomsk, Russia", + "_alias": "Asia/Tomsk", + "_since": "30" + }, + "ruuly": { + "_description": "Ulyanovsk, Russia", + "_alias": "Europe/Ulyanovsk", + "_since": "30" + }, + "ruunera": { + "_description": "Ust-Nera Oymyakonsky, Russia", + "_alias": "Asia/Ust-Nera", + "_since": "23" + }, + "ruuus": { + "_description": "Sakhalin, Russia", + "_alias": "Asia/Sakhalin" + }, + "ruvog": { + "_description": "Volgograd, Russia", + "_alias": "Europe/Volgograd" + }, + "ruvvo": { + "_description": "Vladivostok, Russia", + "_alias": "Asia/Vladivostok" + }, + "ruyek": { + "_description": "Yekaterinburg, Russia", + "_alias": "Asia/Yekaterinburg" + }, + "ruyks": { + "_description": "Yakutsk, Russia", + "_alias": "Asia/Yakutsk" + }, + "rwkgl": { + "_description": "Kigali, Rwanda", + "_alias": "Africa/Kigali" + }, + "saruh": { + "_description": "Riyadh, Saudi Arabia", + "_alias": "Asia/Riyadh" + }, + "sbhir": { + "_description": "Guadalcanal, Solomon Islands", + "_alias": "Pacific/Guadalcanal" + }, + "scmaw": { + "_description": "Mahé, Seychelles", + "_alias": "Indian/Mahe" + }, + "sdkrt": { + "_description": "Khartoum, Sudan", + "_alias": "Africa/Khartoum" + }, + "sesto": { + "_description": "Stockholm, Sweden", + "_alias": "Europe/Stockholm" + }, + "sgsin": { + "_description": "Singapore", + "_alias": "Asia/Singapore Singapore" + }, + "shshn": { + "_description": "Saint Helena", + "_alias": "Atlantic/St_Helena" + }, + "silju": { + "_description": "Ljubljana, Slovenia", + "_alias": "Europe/Ljubljana" + }, + "sjlyr": { + "_description": "Longyearbyen, Svalbard", + "_alias": "Arctic/Longyearbyen Atlantic/Jan_Mayen" + }, + "skbts": { + "_description": "Bratislava, Slovakia", + "_alias": "Europe/Bratislava" + }, + "slfna": { + "_description": "Freetown, Sierra Leone", + "_alias": "Africa/Freetown" + }, + "smsai": { + "_description": "San Marino", + "_alias": "Europe/San_Marino" + }, + "sndkr": { + "_description": "Dakar, Senegal", + "_alias": "Africa/Dakar" + }, + "somgq": { + "_description": "Mogadishu, Somalia", + "_alias": "Africa/Mogadishu" + }, + "srpbm": { + "_description": "Paramaribo, Suriname", + "_alias": "America/Paramaribo" + }, + "ssjub": { + "_description": "Juba, South Sudan", + "_alias": "Africa/Juba", + "_since": "21" + }, + "sttms": { + "_description": "São Tomé, São Tomé and Príncipe", + "_alias": "Africa/Sao_Tome" + }, + "svsal": { + "_description": "El Salvador", + "_alias": "America/El_Salvador" + }, + "sxphi": { + "_description": "Sint Maarten", + "_alias": "America/Lower_Princes", + "_since": "21" + }, + "sydam": { + "_description": "Damascus, Syria", + "_alias": "Asia/Damascus" + }, + "szqmn": { + "_description": "Mbabane, Swaziland", + "_alias": "Africa/Mbabane" + }, + "tcgdt": { + "_description": "Grand Turk, Turks and Caicos Islands", + "_alias": "America/Grand_Turk" + }, + "tdndj": { + "_description": "N'Djamena, Chad", + "_alias": "Africa/Ndjamena" + }, + "tfpfr": { + "_description": "Kerguelen Islands, French Southern Territories", + "_alias": "Indian/Kerguelen" + }, + "tglfw": { + "_description": "Lomé, Togo", + "_alias": "Africa/Lome" + }, + "thbkk": { + "_description": "Bangkok, Thailand", + "_alias": "Asia/Bangkok" + }, + "tjdyu": { + "_description": "Dushanbe, Tajikistan", + "_alias": "Asia/Dushanbe" + }, + "tkfko": { + "_description": "Fakaofo, Tokelau", + "_alias": "Pacific/Fakaofo" + }, + "tldil": { + "_description": "Dili, East Timor", + "_alias": "Asia/Dili" + }, + "tmasb": { + "_description": "Ashgabat, Turkmenistan", + "_alias": "Asia/Ashgabat Asia/Ashkhabad" + }, + "tntun": { + "_description": "Tunis, Tunisia", + "_alias": "Africa/Tunis" + }, + "totbu": { + "_description": "Tongatapu, Tonga", + "_alias": "Pacific/Tongatapu" + }, + "trist": { + "_description": "Istanbul, Türkiye", + "_alias": "Europe/Istanbul Asia/Istanbul Turkey" + }, + "ttpos": { + "_description": "Port of Spain, Trinidad and Tobago", + "_alias": "America/Port_of_Spain" + }, + "tvfun": { + "_description": "Funafuti, Tuvalu", + "_alias": "Pacific/Funafuti" + }, + "twtpe": { + "_description": "Taipei, Taiwan", + "_alias": "Asia/Taipei ROC" + }, + "tzdar": { + "_description": "Dar es Salaam, Tanzania", + "_alias": "Africa/Dar_es_Salaam" + }, + "uaiev": { + "_description": "Kyiv, Ukraine", + "_alias": "Europe/Kiev Europe/Kyiv" + }, + "uaozh": { + "_description": "Zaporizhia (Zaporozhye), Ukraine", + "_alias": "Europe/Zaporozhye" + }, + "uasip": { + "_description": "Simferopol, Ukraine", + "_alias": "Europe/Simferopol" + }, + "uauzh": { + "_description": "Uzhhorod (Uzhgorod), Ukraine", + "_alias": "Europe/Uzhgorod" + }, + "ugkla": { + "_description": "Kampala, Uganda", + "_alias": "Africa/Kampala" + }, + "umawk": { + "_description": "Wake Island, U.S. Minor Outlying Islands", + "_alias": "Pacific/Wake" + }, + "umjon": { + "_description": "Johnston Atoll, U.S. Minor Outlying Islands", + "_alias": "Pacific/Johnston" + }, + "ummdy": { + "_description": "Midway Islands, U.S. Minor Outlying Islands", + "_alias": "Pacific/Midway" + }, + "unk": { + "_description": "Unknown time zone", + "_alias": "Etc/Unknown" + }, + "usadk": { + "_description": "Adak (Alaska), United States", + "_alias": "America/Adak America/Atka US/Aleutian" + }, + "usaeg": { + "_description": "Marengo (Indiana), United States", + "_alias": "America/Indiana/Marengo" + }, + "usanc": { + "_description": "Anchorage, United States", + "_alias": "America/Anchorage US/Alaska" + }, + "usboi": { + "_description": "Boise (Idaho), United States", + "_alias": "America/Boise" + }, + "uschi": { + "_description": "Chicago, United States", + "_alias": "America/Chicago US/Central" + }, + "usden": { + "_description": "Denver, United States", + "_alias": "America/Denver America/Shiprock Navajo US/Mountain" + }, + "usdet": { + "_description": "Detroit, United States", + "_alias": "America/Detroit US/Michigan" + }, + "ushnl": { + "_description": "Honolulu, United States", + "_alias": "Pacific/Honolulu US/Hawaii" + }, + "usind": { + "_description": "Indianapolis, United States", + "_alias": "America/Indianapolis America/Fort_Wayne America/Indiana/Indianapolis US/East-Indiana" + }, + "usinvev": { + "_description": "Vevay (Indiana), United States", + "_alias": "America/Indiana/Vevay" + }, + "usjnu": { + "_description": "Juneau (Alaska), United States", + "_alias": "America/Juneau" + }, + "usknx": { + "_description": "Knox (Indiana), United States", + "_alias": "America/Indiana/Knox America/Knox_IN US/Indiana-Starke" + }, + "uslax": { + "_description": "Los Angeles, United States", + "_alias": "America/Los_Angeles US/Pacific US/Pacific-New" + }, + "uslui": { + "_description": "Louisville (Kentucky), United States", + "_alias": "America/Louisville America/Kentucky/Louisville" + }, + "usmnm": { + "_description": "Menominee (Michigan), United States", + "_alias": "America/Menominee" + }, + "usmoc": { + "_description": "Monticello (Kentucky), United States", + "_alias": "America/Kentucky/Monticello" + }, + "usmtm": { + "_description": "Metlakatla (Alaska), United States", + "_alias": "America/Metlakatla", + "_since": "1.9.1" + }, + "usnavajo": { + "_deprecated": true, + "_description": "Shiprock (Navajo), United States", + "_preferred": "usden" + }, + "usndcnt": { + "_description": "Center (North Dakota), United States", + "_alias": "America/North_Dakota/Center" + }, + "usndnsl": { + "_description": "New Salem (North Dakota), United States", + "_alias": "America/North_Dakota/New_Salem" + }, + "usnyc": { + "_description": "New York, United States", + "_alias": "America/New_York US/Eastern" + }, + "usoea": { + "_description": "Vincennes (Indiana), United States", + "_alias": "America/Indiana/Vincennes" + }, + "usome": { + "_description": "Nome (Alaska), United States", + "_alias": "America/Nome" + }, + "usphx": { + "_description": "Phoenix, United States", + "_alias": "America/Phoenix US/Arizona" + }, + "ussit": { + "_description": "Sitka (Alaska), United States", + "_alias": "America/Sitka", + "_since": "1.9.1" + }, + "ustel": { + "_description": "Tell City (Indiana), United States", + "_alias": "America/Indiana/Tell_City" + }, + "uswlz": { + "_description": "Winamac (Indiana), United States", + "_alias": "America/Indiana/Winamac" + }, + "uswsq": { + "_description": "Petersburg (Indiana), United States", + "_alias": "America/Indiana/Petersburg" + }, + "usxul": { + "_description": "Beulah (North Dakota), United States", + "_alias": "America/North_Dakota/Beulah", + "_since": "1.9.1" + }, + "usyak": { + "_description": "Yakutat (Alaska), United States", + "_alias": "America/Yakutat" + }, + "utc": { + "_description": "UTC (Coordinated Universal Time)", + "_alias": "Etc/UTC Etc/UCT Etc/Universal Etc/Zulu UCT UTC Universal Zulu" + }, + "utce01": { + "_description": "1 hour ahead of UTC", + "_alias": "Etc/GMT-1" + }, + "utce02": { + "_description": "2 hours ahead of UTC", + "_alias": "Etc/GMT-2" + }, + "utce03": { + "_description": "3 hours ahead of UTC", + "_alias": "Etc/GMT-3" + }, + "utce04": { + "_description": "4 hours ahead of UTC", + "_alias": "Etc/GMT-4" + }, + "utce05": { + "_description": "5 hours ahead of UTC", + "_alias": "Etc/GMT-5" + }, + "utce06": { + "_description": "6 hours ahead of UTC", + "_alias": "Etc/GMT-6" + }, + "utce07": { + "_description": "7 hours ahead of UTC", + "_alias": "Etc/GMT-7" + }, + "utce08": { + "_description": "8 hours ahead of UTC", + "_alias": "Etc/GMT-8" + }, + "utce09": { + "_description": "9 hours ahead of UTC", + "_alias": "Etc/GMT-9" + }, + "utce10": { + "_description": "10 hours ahead of UTC", + "_alias": "Etc/GMT-10" + }, + "utce11": { + "_description": "11 hours ahead of UTC", + "_alias": "Etc/GMT-11" + }, + "utce12": { + "_description": "12 hours ahead of UTC", + "_alias": "Etc/GMT-12" + }, + "utce13": { + "_description": "13 hours ahead of UTC", + "_alias": "Etc/GMT-13" + }, + "utce14": { + "_description": "14 hours ahead of UTC", + "_alias": "Etc/GMT-14" + }, + "utcw01": { + "_description": "1 hour behind UTC", + "_alias": "Etc/GMT+1" + }, + "utcw02": { + "_description": "2 hours behind UTC", + "_alias": "Etc/GMT+2" + }, + "utcw03": { + "_description": "3 hours behind UTC", + "_alias": "Etc/GMT+3" + }, + "utcw04": { + "_description": "4 hours behind UTC", + "_alias": "Etc/GMT+4" + }, + "utcw05": { + "_description": "5 hours behind UTC", + "_alias": "Etc/GMT+5 EST" + }, + "utcw06": { + "_description": "6 hours behind UTC", + "_alias": "Etc/GMT+6" + }, + "utcw07": { + "_description": "7 hours behind UTC", + "_alias": "Etc/GMT+7 MST" + }, + "utcw08": { + "_description": "8 hours behind UTC", + "_alias": "Etc/GMT+8" + }, + "utcw09": { + "_description": "9 hours behind UTC", + "_alias": "Etc/GMT+9" + }, + "utcw10": { + "_description": "10 hours behind UTC", + "_alias": "Etc/GMT+10 HST" + }, + "utcw11": { + "_description": "11 hours behind UTC", + "_alias": "Etc/GMT+11" + }, + "utcw12": { + "_description": "12 hours behind UTC", + "_alias": "Etc/GMT+12" + }, + "uymvd": { + "_description": "Montevideo, Uruguay", + "_alias": "America/Montevideo" + }, + "uzskd": { + "_description": "Samarkand, Uzbekistan", + "_alias": "Asia/Samarkand" + }, + "uztas": { + "_description": "Tashkent, Uzbekistan", + "_alias": "Asia/Tashkent" + }, + "vavat": { + "_description": "Vatican City", + "_alias": "Europe/Vatican" + }, + "vcsvd": { + "_description": "Saint Vincent, Saint Vincent and the Grenadines", + "_alias": "America/St_Vincent" + }, + "veccs": { + "_description": "Caracas, Venezuela", + "_alias": "America/Caracas" + }, + "vgtov": { + "_description": "Tortola, British Virgin Islands", + "_alias": "America/Tortola" + }, + "vistt": { + "_description": "Saint Thomas, U.S. Virgin Islands", + "_alias": "America/St_Thomas America/Virgin" + }, + "vnsgn": { + "_description": "Ho Chi Minh City, Vietnam", + "_alias": "Asia/Saigon Asia/Ho_Chi_Minh" + }, + "vuvli": { + "_description": "Efate, Vanuatu", + "_alias": "Pacific/Efate" + }, + "wfmau": { + "_description": "Wallis Islands, Wallis and Futuna", + "_alias": "Pacific/Wallis" + }, + "wsapw": { + "_description": "Apia, Samoa", + "_alias": "Pacific/Apia" + }, + "yeade": { + "_description": "Aden, Yemen", + "_alias": "Asia/Aden" + }, + "ytmam": { + "_description": "Mayotte", + "_alias": "Indian/Mayotte" + }, + "zajnb": { + "_description": "Johannesburg, South Africa", + "_alias": "Africa/Johannesburg" + }, + "zmlun": { + "_description": "Lusaka, Zambia", + "_alias": "Africa/Lusaka" + }, + "zwhre": { + "_description": "Harare, Zimbabwe", + "_alias": "Africa/Harare" + } + } + } + } +} diff --git a/polyfill/test/ecmascript.mjs b/polyfill/test/ecmascript.mjs index 03fc56e7e1..94d3c752b3 100644 --- a/polyfill/test/ecmascript.mjs +++ b/polyfill/test/ecmascript.mjs @@ -8,6 +8,7 @@ import { strict as assert } from 'assert'; const { deepEqual, equal, throws } = assert; import bigInt from 'big-integer'; +import { readFileSync } from 'fs'; import * as ES from '../lib/ecmascript.mjs'; import { GetSlot, TIMEZONE_ID } from '../lib/slots.mjs'; @@ -64,6 +65,61 @@ describe('ECMAScript', () => { }); } }); + + describe('GetAvailableNamedTimeZoneIdentifier', () => { + it('Case-normalizes time zone IDs', () => { + // eslint-disable-next-line max-len + // curl -s https://raw.githubusercontent.com/unicode-org/cldr-json/main/cldr-json/cldr-bcp47/bcp47/timezone.json > cldr-timezone.json + const cldrTimeZonePath = new URL('./cldr-timezone.json', import.meta.url); + const cldrTimeZoneJson = JSON.parse(readFileSync(cldrTimeZonePath)); + + // get CLDR's time zone IDs + const cldrIdentifiers = Object.entries(cldrTimeZoneJson.keyword.u.tz) + .filter((z) => !z[0].startsWith('_')) // ignore metadata elements + .map((z) => z[1]._alias) // pull out the list of IANA IDs for each CLDR zone + .filter(Boolean) // CLDR deprecated zones no longer have an IANA ID + .flatMap((ids) => ids.split(' ')) // expand all space-delimited IANA IDs for each zone + .filter((id) => !['America/Ciudad_Juarez'].includes(id)) // exclude IDs that are too new to be supported + .filter((id) => !['Etc/Unknown'].includes(id)); // see https://github.com/tc39/proposal-canonical-tz/pull/25 + + // These 4 legacy IDs are in TZDB, in Wikipedia, and accepted by ICU, but they're not in CLDR data. + // Not sure where they come from, perhaps hard-coded into ICU, but we'll test them anyway. + const missingFromCLDR = ['CET', 'EET', 'MET', 'WET']; + + // All IDs that we know about + const ids = [...new Set([...missingFromCLDR, ...cldrIdentifiers, ...Intl.supportedValuesOf('timeZone')])]; + + for (const id of ids) { + const lower = id.toLowerCase(); + const upper = id.toUpperCase(); + equal(ES.GetAvailableNamedTimeZoneIdentifier(id)?.identifier, id); + equal(ES.GetAvailableNamedTimeZoneIdentifier(upper)?.identifier, id); + equal(ES.GetAvailableNamedTimeZoneIdentifier(lower)?.identifier, id); + } + }); + it('Returns canonical IDs', () => { + const ids = Intl.supportedValuesOf('timeZone'); + for (const id of ids) { + equal(ES.GetAvailableNamedTimeZoneIdentifier(id).primaryIdentifier, id); + } + const knownAliases = [ + ['America/Atka', 'America/Adak'], + ['America/Knox_IN', 'America/Indiana/Knox'], + ['Asia/Ashkhabad', 'Asia/Ashgabat'], + ['Asia/Dacca', 'Asia/Dhaka'], + ['Asia/Istanbul', 'Europe/Istanbul'], + ['Asia/Macao', 'Asia/Macau'], + ['Asia/Thimbu', 'Asia/Thimphu'], + ['Asia/Ujung_Pandang', 'Asia/Makassar'], + ['Asia/Ulan_Bator', 'Asia/Ulaanbaatar'] + ]; + for (const [identifier, primaryIdentifier] of knownAliases) { + const record = ES.GetAvailableNamedTimeZoneIdentifier(identifier); + equal(record.identifier, identifier); + equal(record.primaryIdentifier, primaryIdentifier); + } + }); + }); }); import { normalize } from 'path';