@@ -360,24 +360,6 @@ export function RejectTemporalLikeObject(item) {
360360 }
361361}
362362
363- export function CanonicalizeTimeZoneOffsetString ( offsetString ) {
364- const offsetNs = ParseTimeZoneOffsetString ( offsetString ) ;
365- return FormatTimeZoneOffsetString ( offsetNs ) ;
366- }
367-
368- export function ParseTemporalTimeZone ( stringIdent ) {
369- const { tzName, offset, z } = ParseTemporalTimeZoneString ( stringIdent ) ;
370- if ( tzName ) {
371- if ( IsTimeZoneOffsetString ( tzName ) ) return CanonicalizeTimeZoneOffsetString ( tzName ) ;
372- const record = GetAvailableNamedTimeZoneIdentifier ( tzName ) ;
373- if ( ! record ) throw new RangeError ( `Unrecognized time zone ${ tzName } ` ) ;
374- return record . primaryIdentifier ;
375- }
376- if ( z ) return 'UTC' ;
377- // if !tzName && !z then offset must be present
378- return CanonicalizeTimeZoneOffsetString ( offset ) ;
379- }
380-
381363export function MaybeFormatCalendarAnnotation ( calendar , showCalendar ) {
382364 if ( showCalendar === 'never' ) return '' ;
383365 return FormatCalendarAnnotation ( ToTemporalCalendarIdentifier ( calendar ) , showCalendar ) ;
@@ -570,19 +552,46 @@ export function ParseTemporalMonthDayString(isoString) {
570552 return { month, day, calendar, referenceISOYear } ;
571553}
572554
555+ const TIMEZONE_IDENTIFIER = new RegExp ( `^${ PARSE . timeZoneID . source } $` , 'i' ) ;
556+ const OFFSET_IDENTIFIER = new RegExp ( `^${ PARSE . offsetIdentifier . source } $` ) ;
557+
558+ export function ParseTimeZoneIdentifier ( identifier ) {
559+ if ( ! TIMEZONE_IDENTIFIER . test ( identifier ) ) return undefined ;
560+ if ( OFFSET_IDENTIFIER . test ( identifier ) ) {
561+ // The regex limits the input to minutes precision
562+ const { offsetNanoseconds } = ParseUTCOffsetString ( identifier ) ;
563+ return { offsetMinutes : offsetNanoseconds / 6e10 } ;
564+ }
565+ return { tzName : identifier } ;
566+ }
567+
573568export function ParseTemporalTimeZoneString ( stringIdent ) {
574569 const bareID = new RegExp ( `^${ PARSE . timeZoneID . source } $` , 'i' ) ;
575- if ( bareID . test ( stringIdent ) ) return { tzName : stringIdent } ;
570+ if ( bareID . test ( stringIdent ) ) {
571+ const identifierParseResult = ParseTimeZoneIdentifier ( stringIdent ) ;
572+ if ( ! identifierParseResult ) throw new RangeError ( `Invalid time zone: ${ stringIdent } ` ) ;
573+ return identifierParseResult ;
574+ }
575+
576+ // Try parsing ISO string instead
577+ let z , offset , tzName ;
576578 try {
577- // Try parsing ISO string instead
578- const result = ParseISODateTime ( stringIdent ) ;
579- if ( result . z || result . offset || result . tzName ) {
580- return result ;
581- }
579+ ( { z, offset, tzName } = ParseISODateTime ( stringIdent ) ) ;
582580 } catch {
583- // fall through
581+ throw new RangeError ( `Invalid time zone: ${ stringIdent } ` ) ;
582+ }
583+ if ( tzName ) {
584+ const identifierParseResult = ParseTimeZoneIdentifier ( tzName ) ;
585+ if ( ! identifierParseResult ) throw new RangeError ( `Invalid time zone: ${ tzName } ` ) ;
586+ return identifierParseResult ;
587+ }
588+ if ( z ) return { tzName : 'UTC' } ;
589+ // if !tzName and !z then offset must be present
590+ const offsetParseResult = ParseUTCOffsetString ( offset ) ;
591+ if ( offsetParseResult . hasSubMinutePrecision ) {
592+ throw new RangeError ( `Seconds not allowed in offset time zone: ${ offset } ` ) ;
584593 }
585- throw new RangeError ( `Invalid time zone: ${ stringIdent } ` ) ;
594+ return { offsetMinutes : offsetParseResult . offsetNanoseconds / 6e10 } ;
586595}
587596
588597export function ParseTemporalDurationString ( isoString ) {
@@ -641,7 +650,7 @@ export function ParseTemporalInstant(isoString) {
641650 ParseTemporalInstantString ( isoString ) ;
642651
643652 if ( ! z && ! offset ) throw new RangeError ( 'Temporal.Instant requires a time zone offset' ) ;
644- const offsetNs = z ? 0 : ParseTimeZoneOffsetString ( offset ) ;
653+ const offsetNs = z ? 0 : ParseUTCOffsetString ( offset ) . offsetNanoseconds ;
645654 ( { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime (
646655 year ,
647656 month ,
@@ -1005,7 +1014,7 @@ export function ToRelativeTemporalObject(options) {
10051014 calendar = ASCIILowercase ( calendar ) ;
10061015 }
10071016 if ( timeZone === undefined ) return CreateTemporalDate ( year , month , day , calendar ) ;
1008- const offsetNs = offsetBehaviour === 'option' ? ParseTimeZoneOffsetString ( offset ) : 0 ;
1017+ const offsetNs = offsetBehaviour === 'option' ? ParseUTCOffsetString ( offset ) . offsetNanoseconds : 0 ;
10091018 const epochNanoseconds = InterpretISODateTimeOffset (
10101019 year ,
10111020 month ,
@@ -1469,7 +1478,7 @@ export function ToTemporalZonedDateTime(item, options) {
14691478 ToTemporalOverflow ( options ) ; // validate and ignore
14701479 }
14711480 let offsetNs = 0 ;
1472- if ( offsetBehaviour === 'option' ) offsetNs = ParseTimeZoneOffsetString ( offset ) ;
1481+ if ( offsetBehaviour === 'option' ) offsetNs = ParseUTCOffsetString ( offset ) . offsetNanoseconds ;
14731482 const epochNanoseconds = InterpretISODateTimeOffset (
14741483 year ,
14751484 month ,
@@ -2099,7 +2108,12 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) {
20992108 return temporalTimeZoneLike ;
21002109 }
21012110 const identifier = ToString ( temporalTimeZoneLike ) ;
2102- return ParseTemporalTimeZone ( identifier ) ;
2111+ const { tzName, offsetMinutes } = ParseTemporalTimeZoneString ( identifier ) ;
2112+ if ( offsetMinutes !== undefined ) return FormatTimeZoneOffsetString ( Math . round ( offsetMinutes * 6e10 ) ) ; // TODO: -round
2113+
2114+ const record = GetAvailableNamedTimeZoneIdentifier ( tzName ) ;
2115+ if ( ! record ) throw new RangeError ( `Unrecognized time zone ${ tzName } ` ) ;
2116+ return record . primaryIdentifier ;
21032117}
21042118
21052119export function ToTemporalTimeZoneIdentifier ( slotValue ) {
@@ -2575,11 +2589,11 @@ export function TemporalZonedDateTimeToString(
25752589 return result ;
25762590}
25772591
2578- export function IsTimeZoneOffsetString ( string ) {
2592+ export function IsOffsetTimeZoneIdentifier ( string ) {
25792593 return OFFSET . test ( string ) ;
25802594}
25812595
2582- export function ParseTimeZoneOffsetString ( string ) {
2596+ export function ParseUTCOffsetString ( string ) {
25832597 const match = OFFSET . exec ( string ) ;
25842598 if ( ! match ) {
25852599 throw new RangeError ( `invalid time zone offset: ${ string } ` ) ;
@@ -2589,7 +2603,9 @@ export function ParseTimeZoneOffsetString(string) {
25892603 const minutes = + ( match [ 3 ] || 0 ) ;
25902604 const seconds = + ( match [ 4 ] || 0 ) ;
25912605 const nanoseconds = + ( ( match [ 5 ] || 0 ) + '000000000' ) . slice ( 0 , 9 ) ;
2592- return sign * ( ( ( hours * 60 + minutes ) * 60 + seconds ) * 1e9 + nanoseconds ) ;
2606+ const offsetNanoseconds = sign * ( ( ( hours * 60 + minutes ) * 60 + seconds ) * 1e9 + nanoseconds ) ;
2607+ const hasSubMinutePrecision = match [ 4 ] !== undefined || match [ 5 ] !== undefined ;
2608+ return { offsetNanoseconds, hasSubMinutePrecision } ;
25932609}
25942610
25952611let canonicalTimeZoneIdsCache = undefined ;
@@ -2726,14 +2742,7 @@ export function FormatTimeZoneOffsetString(offsetNanoseconds) {
27262742
27272743export function FormatISOTimeZoneOffsetString ( offsetNanoseconds ) {
27282744 offsetNanoseconds = RoundNumberToIncrement ( bigInt ( offsetNanoseconds ) , 60e9 , 'halfExpand' ) . toJSNumber ( ) ;
2729- const sign = offsetNanoseconds < 0 ? '-' : '+' ;
2730- offsetNanoseconds = MathAbs ( offsetNanoseconds ) ;
2731- const minutes = ( offsetNanoseconds / 60e9 ) % 60 ;
2732- const hours = MathFloor ( offsetNanoseconds / 3600e9 ) ;
2733-
2734- const hourString = ISODateTimePartString ( hours ) ;
2735- const minuteString = ISODateTimePartString ( minutes ) ;
2736- return `${ sign } ${ hourString } :${ minuteString } ` ;
2745+ return FormatTimeZoneOffsetString ( offsetNanoseconds ) ;
27372746}
27382747
27392748export function GetUTCEpochNanoseconds ( year , month , day , hour , minute , second , millisecond , microsecond , nanosecond ) {
0 commit comments