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~
@@ -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) }.