From f6818ce02da8ffab158a1773571f1e87ab580d31 Mon Sep 17 00:00:00 2001 From: Justin Grant Date: Sat, 17 Jun 2023 17:38:26 -0700 Subject: [PATCH] Normative: Limit offset time zones to minutes --- polyfill/lib/ecmascript.mjs | 12 +++++--- polyfill/lib/regex.mjs | 2 +- polyfill/lib/timezone.mjs | 14 +++++----- polyfill/test/validStrings.mjs | 6 +--- spec/abstractops.html | 2 +- spec/intl.html | 2 +- spec/mainadditions.html | 2 ++ spec/timezone.html | 50 +++++++++++++++++----------------- 8 files changed, 46 insertions(+), 44 deletions(-) diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index b1dd2d60aa..e8a29a263b 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -558,8 +558,9 @@ const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`); export function ParseTimeZoneIdentifier(identifier) { if (!TIMEZONE_IDENTIFIER.test(identifier)) return undefined; if (OFFSET_IDENTIFIER.test(identifier)) { + // The regex limits the input to minutes precision const { offsetNanoseconds } = ParseUTCOffsetString(identifier); - return { offsetNanoseconds }; + return { offsetMinutes: offsetNanoseconds / 6e10 }; } return { tzName: identifier }; } @@ -2096,8 +2097,8 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) { const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier); if (tzName) { // tzName is any valid identifier string in brackets, and could be an offset identifier - const { offsetNanoseconds } = ParseTimeZoneIdentifier(tzName); - if (offsetNanoseconds !== undefined) return FormatTimeZoneOffsetString(offsetNanoseconds); + const { offsetMinutes } = ParseTimeZoneIdentifier(tzName); + if (offsetMinutes !== undefined) return FormatTimeZoneOffsetString(offsetMinutes * 6e10); const record = GetAvailableNamedTimeZoneIdentifier(tzName); if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`); @@ -2105,7 +2106,10 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) { } if (z) return 'UTC'; // if !tzName && !z then offset must be present - const { offsetNanoseconds } = ParseUTCOffsetString(offset); + const { offsetNanoseconds, hasSubMinutePrecision } = ParseUTCOffsetString(offset); + if (hasSubMinutePrecision) { + throw new RangeError(`Seconds not allowed in offset time zone: ${offset}`); + } return FormatTimeZoneOffsetString(offsetNanoseconds); } diff --git a/polyfill/lib/regex.mjs b/polyfill/lib/regex.mjs index e07db6dffb..8a71c63841 100644 --- a/polyfill/lib/regex.mjs +++ b/polyfill/lib/regex.mjs @@ -24,7 +24,7 @@ export const datesplit = new RegExp( const timesplit = /(\d{2})(?::(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?|(\d{2})(?:(\d{2})(?:[.,](\d{1,9}))?)?)?/; export const offset = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])(?::?([0-5][0-9])(?:[.,](\d{1,9}))?)?)?/; const offsetpart = new RegExp(`([zZ])|${offset.source}?`); -export const offsetIdentifier = offset; +export const offsetIdentifier = /([+\u2212-])([01][0-9]|2[0-3])(?::?([0-5][0-9])?)?/; export const annotation = /\[(!)?([a-z_][a-z0-9_-]*)=([A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\]/g; export const zoneddatetime = new RegExp( diff --git a/polyfill/lib/timezone.mjs b/polyfill/lib/timezone.mjs index d017607225..57257ac476 100644 --- a/polyfill/lib/timezone.mjs +++ b/polyfill/lib/timezone.mjs @@ -28,8 +28,8 @@ export class TimeZone { } let stringIdentifier = ES.ToString(identifier); const parseResult = ES.ParseTimeZoneIdentifier(identifier); - if (parseResult?.offsetNanoseconds !== undefined) { - stringIdentifier = ES.FormatTimeZoneOffsetString(parseResult.offsetNanoseconds); + if (parseResult?.offsetMinutes !== undefined) { + stringIdentifier = ES.FormatTimeZoneOffsetString(parseResult.offsetMinutes * 6e10); } else { const record = ES.GetAvailableNamedTimeZoneIdentifier(stringIdentifier); if (!record) throw new RangeError(`Invalid time zone identifier: ${stringIdentifier}`); @@ -56,8 +56,8 @@ export class TimeZone { instant = ES.ToTemporalInstant(instant); const id = GetSlot(this, TIMEZONE_ID); - const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id)?.offsetNanoseconds; - if (offsetNanoseconds !== undefined) return offsetNanoseconds; + const offsetMinutes = ES.ParseTimeZoneIdentifier(id)?.offsetMinutes; + if (offsetMinutes !== undefined) return offsetMinutes * 6e10; return ES.GetNamedTimeZoneOffsetNanoseconds(id, GetSlot(instant, EPOCHNANOSECONDS)); } @@ -85,8 +85,8 @@ export class TimeZone { const Instant = GetIntrinsic('%Temporal.Instant%'); const id = GetSlot(this, TIMEZONE_ID); - const offsetNanoseconds = ES.ParseTimeZoneIdentifier(id)?.offsetNanoseconds; - if (offsetNanoseconds !== undefined) { + const offsetMinutes = ES.ParseTimeZoneIdentifier(id)?.offsetMinutes; + if (offsetMinutes !== undefined) { const epochNs = ES.GetUTCEpochNanoseconds( GetSlot(dateTime, ISO_YEAR), GetSlot(dateTime, ISO_MONTH), @@ -99,7 +99,7 @@ export class TimeZone { GetSlot(dateTime, ISO_NANOSECOND) ); if (epochNs === null) throw new RangeError('DateTime outside of supported range'); - return [new Instant(epochNs.minus(offsetNanoseconds))]; + return [new Instant(epochNs.minus(offsetMinutes * 6e10))]; } const possibleEpochNs = ES.GetNamedTimeZoneEpochNanoseconds( diff --git a/polyfill/test/validStrings.mjs b/polyfill/test/validStrings.mjs index 97381f08e0..762f8dd1c2 100644 --- a/polyfill/test/validStrings.mjs +++ b/polyfill/test/validStrings.mjs @@ -262,11 +262,7 @@ const utcOffset = withCode( saveOffset ); const timeZoneUTCOffset = choice(utcDesignator, utcOffset); -const timeZoneUTCOffsetName = seq( - sign, - hour, - choice([minuteSecond, [minuteSecond, [fraction]]], seq(':', minuteSecond, [':', minuteSecond, [fraction]])) -); +const timeZoneUTCOffsetName = seq(sign, hour, choice([minuteSecond], seq(':', minuteSecond))); const timeZoneIANAName = choice(...timezoneNames); const timeZoneIdentifier = withCode( choice(timeZoneUTCOffsetName, timeZoneIANAName), diff --git a/spec/abstractops.html b/spec/abstractops.html index 7dbbab0517..e90bbe3cea 100644 --- a/spec/abstractops.html +++ b/spec/abstractops.html @@ -1121,7 +1121,7 @@

ISO 8601 grammar

UTCOffsetSubMinutePrecision TimeZoneUTCOffsetName : - UTCOffsetSubMinutePrecision + UTCOffsetMinutePrecision TZLeadingChar : Alpha diff --git a/spec/intl.html b/spec/intl.html index 7fddd9d18c..1578ebf7a3 100644 --- a/spec/intl.html +++ b/spec/intl.html @@ -2543,7 +2543,7 @@

Temporal.ZonedDateTime.prototype.toLocaleString ( [ _locales_ [ , _options_ 1. Let _dateTimeFormat_ be ! OrdinaryCreateFromConstructor(%DateTimeFormat%, %DateTimeFormat.prototype%, « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »). 1. Let _timeZone_ be ? ToTemporalTimeZoneIdentifier(_zonedDateTime_.[[TimeZone]]). 1. Let _timeZoneParseResult_ be ParseTimeZoneIdentifier(_timeZone_). - 1. If _timeZoneParseResult_ is not ~unmatched~ and _timeZoneParseResult_.[[OffsetNanoseconds]] is not ~empty~, throw a *RangeError* exception. + 1. If _timeZoneParseResult_ is not ~unmatched~ and _timeZoneParseResult_.[[OffsetMinutes]] is not ~empty~, throw a *RangeError* exception. 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_timeZone_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. 1. Set _timeZone_ to _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. diff --git a/spec/mainadditions.html b/spec/mainadditions.html index fd566195cd..8f7efeb8b6 100644 --- a/spec/mainadditions.html +++ b/spec/mainadditions.html @@ -311,6 +311,8 @@

Time Zone Offset String FormatFormats

ECMAScript defines string interchange formats for UTC offsets, derived from ISO 8601. + UTC offsets that represent offset time zone identifiers, or that are intended for interoperability with ISO 8601, use only hours and minutes and are specified by |UTCOffsetMinutePrecision|. + UTC offsets that represent the offset of a named or custom time zone can be more precise, and are specified by |UTCOffsetSubMinutePrecision|.

These formats are described by the ISO String grammar in . diff --git a/spec/timezone.html b/spec/timezone.html index 31a026f2ce..78c59c689e 100644 --- a/spec/timezone.html +++ b/spec/timezone.html @@ -34,8 +34,8 @@

Temporal.TimeZone ( _identifier_ )

1. Set _identifier_ to ? ToString(_identifier_). 1. Let _parseResult_ be ParseTimeZoneIdentifier(_identifier_). 1. If _parseResult_ is ~unmatched~, throw a *RangeError* exception. - 1. If _parseResult_.[[OffsetNanoseconds]] is not ~empty~, then - 1. Set _identifier_ to FormatTimeZoneOffsetString(_parseResult_.[[OffsetNanoseconds]]). + 1. If _parseResult_.[[OffsetMinutes]] is not ~empty~, then + 1. Set _identifier_ to FormatTimeZoneOffsetString(_parseResult_.[[OffsetMinutes]] × (6 × 1010)). 1. Else, 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_identifier_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. @@ -102,7 +102,7 @@

get Temporal.TimeZone.prototype.id

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetMinutes]] × (6 × 1010)). 1. Return _timeZone_.[[Identifier]]. @@ -116,7 +116,7 @@

Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( _instant_ )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). 1. Set _instant_ to ? ToTemporalInstant(_instant_). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return 𝔽(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return 𝔽(_timeZone_.[[OffsetMinutes]]). 1. Return 𝔽(GetNamedTimeZoneOffsetNanoseconds(_timeZone_.[[Identifier]], _instant_.[[Nanoseconds]])). @@ -172,9 +172,9 @@

Temporal.TimeZone.prototype.getPossibleInstantsFor ( _dateTime_ )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). 1. Set _dateTime_ to ? ToTemporalDateTime(_dateTime_). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, then + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, then 1. Let _epochNanoseconds_ be GetUTCEpochNanoseconds(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]]). - 1. Let _possibleEpochNanoseconds_ be « _epochNanoseconds_ - ℤ(_timeZone_.[[OffsetNanoseconds]]) ». + 1. Let _possibleEpochNanoseconds_ be « _epochNanoseconds_ - ℤ(_timeZone_.[[OffsetMinutes]] × (6 × 1010)) ». 1. Else, 1. Let _possibleEpochNanoseconds_ be GetNamedTimeZoneEpochNanoseconds(_timeZone_.[[Identifier]], _dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]]). 1. Let _possibleInstants_ be a new empty List. @@ -195,7 +195,7 @@

Temporal.TimeZone.prototype.getNextTransition ( _startingPoint_ )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). 1. Set _startingPoint_ to ? ToTemporalInstant(_startingPoint_). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return *null*. + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return *null*. 1. Let _transition_ be GetNamedTimeZoneNextTransition(_timeZone_.[[Identifier]], _startingPoint_.[[Nanoseconds]]). 1. If _transition_ is *null*, return *null*. 1. Return ! CreateTemporalInstant(_transition_). @@ -211,7 +211,7 @@

Temporal.TimeZone.prototype.getPreviousTransition ( _startingPoint_ )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). 1. Set _startingPoint_ to ? ToTemporalInstant(_startingPoint_). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return *null*. + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return *null*. 1. Let _transition_ be GetNamedTimeZonePreviousTransition(_timeZone_.[[Identifier]], _startingPoint_.[[Nanoseconds]]). 1. If _transition_ is *null*, return *null*. 1. Return ! CreateTemporalInstant(_transition_). @@ -226,7 +226,7 @@

Temporal.TimeZone.prototype.toString ( )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetMinutes]] × (6 × 1010)). 1. Return _timeZone_.[[Identifier]]. @@ -239,7 +239,7 @@

Temporal.TimeZone.prototype.toJSON ( )

1. Let _timeZone_ be the *this* value. 1. Perform ? RequireInternalSlot(_timeZone_, [[InitializedTemporalTimeZone]]). - 1. If _timeZone_.[[OffsetNanoseconds]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetNanoseconds]]). + 1. If _timeZone_.[[OffsetMinutes]] is not ~empty~, return FormatTimeZoneOffsetString(_timeZone_.[[OffsetMinutes]] × (6 × 1010)). 1. Return _timeZone_.[[Identifier]]. @@ -281,11 +281,11 @@

Properties of Temporal.TimeZone Instances

- [[OffsetNanoseconds]] + [[OffsetMinutes]] - An integer for nanoseconds representing the constant offset of this time zone relative to UTC, or ~empty~ if the instance represents a named time zone. - If not ~empty~, this value must be greater than −8.64 × 1013 nanoseconds (−24 hours) and smaller than +8.64 × 1013 nanoseconds (+24 hours). + An integer for minutes representing the constant offset of this time zone relative to UTC, or ~empty~ if the instance represents a named time zone. + If not ~empty~, this value must be greater than −1440 minutes (−24 hours) and smaller than +1440 minutes (+24 hours). @@ -322,18 +322,18 @@

1. If _newTarget_ is not present, set _newTarget_ to %Temporal.TimeZone%. - 1. Let _object_ be ? OrdinaryCreateFromConstructor(_newTarget_, *"%Temporal.TimeZone.prototype%"*, « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetNanoseconds]] »). + 1. Let _object_ be ? OrdinaryCreateFromConstructor(_newTarget_, *"%Temporal.TimeZone.prototype%"*, « [[InitializedTemporalTimeZone]], [[Identifier]], [[OffsetMinutes]] »). 1. Assert: _identifier_ is an available named time zone identifier or an offset time zone identifier. 1. Let _parseResult_ be ParseTimeZoneIdentifier(_identifier_). 1. Assert: _parseResult_ is not ~unmatched~. - 1. If _parseResult_.[[OffsetNanoseconds]] is not ~empty~, then + 1. If _parseResult_.[[OffsetMinutes]] is not ~empty~, then 1. Set _object_.[[Identifier]] to ~empty~. - 1. Set _object_.[[OffsetNanoseconds]] to _parseResult_.[[OffsetNanoseconds]]. + 1. Set _object_.[[OffsetMinutes]] to _parseResult_.[[OffsetMinutes]]. 1. Else, 1. Assert: _parseResult_.[[Name]] is not ~empty~. 1. Assert: GetAvailableNamedTimeZoneIdentifier(_identifier_).[[PrimaryIdentifier]] is _identifier_. 1. Set _object_.[[Identifier]] to _identifier_. - 1. Set _object_.[[OffsetNanoseconds]] to ~empty~. + 1. Set _object_.[[OffsetMinutes]] to ~empty~. 1. Return _object_. @@ -344,7 +344,7 @@

Although the [[Identifier]] internal slot is a String in this specification, implementations may choose to store named time zone identifiers it in any other form (for example as an enumeration or index into a List of identifier strings) as long as the String can be regenerated when needed.

- Similar flexibility exists for the storage of the [[OffsetNanoseconds]] internal slot, which can be interchangeably represented as a 6-byte integer or as a String value that may be as long as 19 characters. + Similar flexibility exists for the storage of the [[OffsetMinutes]] internal slot, which can be interchangeably represented as a 12-bit integer or as a 6-character ±HH:MM string. ParseTimeZoneIdentifier and FormatTimeZoneOffsetString may be used to losslessly convert one representation to the other. Implementations are free to store either or both representations.

@@ -563,8 +563,8 @@

1. Let _parseResult_ be ? ParseTemporalTimeZoneString(_identifier_). 1. If _parseResult_.[[Name]] is not *undefined*, then 1. Let _name_ be _parseResult_.[[Name]]. - 1. Let _offsetNanoseconds_ be ParseTimeZoneIdentifier(_name_).[[OffsetNanoseconds]]. - 1. If _offsetNanoseconds_ is not ~empty~, return FormatTimeZoneOffsetString(_offsetNanoseconds_). + 1. Let _offsetMinutes_ be ParseTimeZoneIdentifier(_name_).[[OffsetMinutes]]. + 1. If _offsetMinutes_ is not ~empty~, return FormatTimeZoneOffsetString(_offsetMinutes_ × (6 × 1010)). 1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_name_). 1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception. 1. Return _timeZoneIdentifierRecord_.[[PrimaryIdentifier]]. @@ -815,13 +815,13 @@

ParseTimeZoneIdentifier ( _identifier_: a String - ): a Record containing [[Name]] and [[OffsetNanoseconds]] fields, or ~unmatched~ + ): a Record containing [[Name]] and [[OffsetMinutes]] fields, or ~unmatched~

description
- If _identifier_ is an available named time zone identifier, [[Name]] will be _identifier_ and [[OffsetNanoseconds]] will be ~empty~. - If _identifier_ is an offset time zone identifier, [[Name]] will be ~empty~ and [[OffsetNanoseconds]] will be a signed integer. + If _identifier_ is an available named time zone identifier, [[Name]] will be _identifier_ and [[OffsetMinutes]] will be ~empty~. + If _identifier_ is an offset time zone identifier, [[Name]] will be ~empty~ and [[OffsetMinutes]] will be a signed integer. Otherwise, ~unmatched~ will be returned.
@@ -830,13 +830,13 @@

1. If _parseResult_ is a List of errors, return ~unmatched~. 1. If _parseResult_ contains a |TimeZoneIANAName| Parse Node, then 1. Let _name_ be the source text matched by the |TimeZoneIANAName| Parse Node contained within _parseResult_. - 1. Return the Record { [[Name]]: _name_, [[OffsetNanoseconds]]: ~empty~ }. + 1. Return the Record { [[Name]]: _name_, [[OffsetMinutes]]: ~empty~ }. 1. Else, 1. Assert: _parseResult_ contains a |TimeZoneUTCOffsetName| Parse Node. 1. Let _offsetString_ be the source text matched by the |TimeZoneUTCOffsetName| Parse Node contained within _parseResult_. 1. Let _offsetNanoseconds_ be ParseUTCOffsetString(_offsetString_). 1. Assert: _offsetNanoseconds_ modulo 6 × 1010 = 0. - 1. Return the Record { [[Name]]: ~empty~, [[OffsetNanoseconds]]: _offsetNanoseconds_ }. + 1. Return the Record { [[Name]]: ~empty~, [[OffsetMinutes]]: _offsetNanoseconds_ / (6 × 1010) }.