@@ -4,6 +4,7 @@ const ArrayIncludes = Array.prototype.includes;
44const ArrayPrototypePush = Array . prototype . push ;
55const ArrayPrototypeSort = Array . prototype . sort ;
66const IntlDateTimeFormat = globalThis . Intl . DateTimeFormat ;
7+ const IntlSupportedValuesOf = globalThis . Intl . supportedValuesOf ;
78const MathAbs = Math . abs ;
89const MathFloor = Math . floor ;
910const MathMax = Math . max ;
@@ -368,7 +369,9 @@ export function ParseTemporalTimeZone(stringIdent) {
368369 const { tzName, offset, z } = ParseTemporalTimeZoneString ( stringIdent ) ;
369370 if ( tzName ) {
370371 if ( IsTimeZoneOffsetString ( tzName ) ) return CanonicalizeTimeZoneOffsetString ( tzName ) ;
371- return GetCanonicalTimeZoneIdentifier ( tzName ) ;
372+ const record = GetAvailableNamedTimeZoneIdentifier ( tzName ) ;
373+ if ( ! record ) throw new RangeError ( `Unrecognized time zone ${ tzName } ` ) ;
374+ return record . primaryIdentifier ;
372375 }
373376 if ( z ) return 'UTC' ;
374377 // if !tzName && !z then offset must be present
@@ -2589,15 +2592,92 @@ export function ParseTimeZoneOffsetString(string) {
25892592 return sign * ( ( ( hours * 60 + minutes ) * 60 + seconds ) * 1e9 + nanoseconds ) ;
25902593}
25912594
2592- // In the spec, GetCanonicalTimeZoneIdentifier is infallible and is always
2593- // preceded by a call to IsAvailableTimeZoneName. However in the polyfill,
2594- // we don't (yet) have a way to check if a time zone ID is valid without
2595- // also canonicalizing it. So we combine both operations into one function,
2596- // which will return the canonical ID if the ID is valid, and will throw
2597- // if it's not.
2598- export function GetCanonicalTimeZoneIdentifier ( timeZoneIdentifier ) {
2599- const formatter = getIntlDateTimeFormatEnUsForTimeZone ( timeZoneIdentifier ) ;
2600- return formatter . resolvedOptions ( ) . timeZone ;
2595+ let canonicalTimeZoneIdsCache = undefined ;
2596+
2597+ export function GetAvailableNamedTimeZoneIdentifier ( identifier ) {
2598+ // TODO: should there be an assertion here that IsTimeZoneOffsetString returns false?
2599+ if ( IsTimeZoneOffsetString ( identifier ) ) return undefined ;
2600+
2601+ // The most common case is when the identifier is a canonical time zone ID.
2602+ // Fast-path that case by caching a list of canonical IDs. If this is an old
2603+ // ECMAScript implementation that doesn't have this API, we'll set the cache
2604+ // to `null` so we won't bother trying again.
2605+ if ( canonicalTimeZoneIdsCache === undefined ) {
2606+ const canonicalTimeZoneIds = IntlSupportedValuesOf ?. ( 'timeZone' ) ?? null ;
2607+ if ( canonicalTimeZoneIds ) {
2608+ const entries = canonicalTimeZoneIds . map ( ( id ) => [ ASCIILowercase ( id ) , id ] ) ;
2609+ canonicalTimeZoneIdsCache = new Map ( entries ) ;
2610+ }
2611+ }
2612+
2613+ const lower = ASCIILowercase ( identifier ) ;
2614+ let primaryIdentifier = canonicalTimeZoneIdsCache ?. get ( lower ) ;
2615+ if ( primaryIdentifier ) return { identifier : primaryIdentifier , primaryIdentifier } ;
2616+
2617+ // It's not already a primary identifier, so get its primary identifier (or
2618+ // return if it's not an available named time zone ID).
2619+ try {
2620+ const formatter = getIntlDateTimeFormatEnUsForTimeZone ( identifier ) ;
2621+ primaryIdentifier = formatter . resolvedOptions ( ) . timeZone ;
2622+ } catch {
2623+ return undefined ;
2624+ }
2625+
2626+ // The identifier is an alias (a deprecated identifier that's a synonym for
2627+ // a primary identifier), so we need to case-normalize the identifier to
2628+ // match the IANA TZDB, e.g. america/new_york => America/New_York. There's
2629+ // no built-in way to do this using Intl.DateTimeFormat, but the we can
2630+ // normalize almost all aliases (modulo a few special cases) using the
2631+ // TZDB's basic capitalization pattern:
2632+ // 1. capitalize the first letter of the identifier
2633+ // 2. capitalize the letter after every slash, dash, or underscore delimiter
2634+ const standardCase = [ ...lower ]
2635+ . map ( ( c , i ) => ( i === 0 || '/-_' . includes ( lower [ i - 1 ] ) ? c . toUpperCase ( ) : c ) )
2636+ . join ( '' ) ;
2637+ const segments = standardCase . split ( '/' ) ;
2638+
2639+ if ( segments . length === 1 ) {
2640+ // For single-segment legacy IDs, if it's 2-3 chars or contains a number or dash, then
2641+ // (except for the "GB-Eire" special case) the case-normalized form is all uppercase.
2642+ // GMT+0, GMT-0, ACT, LHI, NSW, GB, NZ, PRC, ROC, ROK, UCT, GMT, GMT0,
2643+ // CET, CST6CDT, EET, EST, HST, MET, MST, MST7MDT, PST8PDT, WET, NZ-CHAT, W-SU
2644+ // Otherwise it's the standard form: first letter capitalized, e.g. Iran, Egypt, Hongkong
2645+ if ( lower === 'gb-eire' ) return { identifier : 'GB-Eire' , primaryIdentifier } ;
2646+ return {
2647+ identifier : lower . length <= 3 || / [ - 0 - 9 ] / . test ( lower ) ? lower . toUpperCase ( ) : segments [ 0 ] ,
2648+ primaryIdentifier
2649+ } ;
2650+ }
2651+
2652+ // All Etc zone names are upper case except a few exceptions.
2653+ if ( segments [ 0 ] === 'Etc' ) {
2654+ const etcName = [ 'Zulu' , 'Greenwich' , 'Universal' ] . includes ( segments [ 1 ] ) ? segments [ 1 ] : segments [ 1 ] . toUpperCase ( ) ;
2655+ return { identifier : `Etc/${ etcName } ` , primaryIdentifier } ;
2656+ }
2657+
2658+ // Legacy US identifiers like US/Alaska or US/Indiana-Starke. They're always 2 segments and use standard case.
2659+ if ( segments [ 0 ] === 'Us' ) return { identifier : `US/${ segments [ 1 ] } ` , primaryIdentifier } ;
2660+
2661+ // For multi-segment IDs, there's a few special cases in the second/third segments
2662+ const specialCases = {
2663+ Act : 'ACT' ,
2664+ Lhi : 'LHI' ,
2665+ Nsw : 'NSW' ,
2666+ Dar_Es_Salaam : 'Dar_es_Salaam' ,
2667+ Port_Of_Spain : 'Port_of_Spain' ,
2668+ Isle_Of_Man : 'Isle_of_Man' ,
2669+ Comodrivadavia : 'ComodRivadavia' ,
2670+ Knox_In : 'Knox_IN' ,
2671+ Dumontdurville : 'DumontDUrville' ,
2672+ Mcmurdo : 'McMurdo' ,
2673+ Denoronha : 'DeNoronha' ,
2674+ Easterisland : 'EasterIsland' ,
2675+ Bajanorte : 'BajaNorte' ,
2676+ Bajasur : 'BajaSur'
2677+ } ;
2678+ segments [ 1 ] = specialCases [ segments [ 1 ] ] ?? segments [ 1 ] ;
2679+ if ( segments . length > 2 ) segments [ 2 ] = specialCases [ segments [ 2 ] ] ?? segments [ 2 ] ;
2680+ return { identifier : segments . join ( '/' ) , primaryIdentifier } ;
26012681}
26022682
26032683export function GetNamedTimeZoneOffsetNanoseconds ( id , epochNanoseconds ) {
0 commit comments