Skip to content

Commit

Permalink
Normative: Limit offset time zones to minutes
Browse files Browse the repository at this point in the history
  • Loading branch information
justingrant committed Jun 18, 2023
1 parent abf3db8 commit f6818ce
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 44 deletions.
12 changes: 8 additions & 4 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
Expand Down Expand Up @@ -2096,16 +2097,19 @@ 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}`);
return record.primaryIdentifier;
}
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);
}

Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 7 additions & 7 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand All @@ -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));
}
Expand Down Expand Up @@ -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),
Expand All @@ -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(
Expand Down
6 changes: 1 addition & 5 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 1 addition & 1 deletion spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -1121,7 +1121,7 @@ <h1>ISO 8601 grammar</h1>
UTCOffsetSubMinutePrecision

TimeZoneUTCOffsetName :
UTCOffsetSubMinutePrecision
UTCOffsetMinutePrecision

TZLeadingChar :
Alpha
Expand Down
2 changes: 1 addition & 1 deletion spec/intl.html
Original file line number Diff line number Diff line change
Expand Up @@ -2543,7 +2543,7 @@ <h1>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]].
Expand Down
2 changes: 2 additions & 0 deletions spec/mainadditions.html
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ <h1>Time Zone Offset String <del>Format</del><ins>Formats</ins></h1>
<ins class="block">
<p>
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|.
</p>
<p>
These formats are described by the ISO String grammar in <emu-xref href="#sec-temporal-iso8601grammar"></emu-xref>.
Expand Down
50 changes: 25 additions & 25 deletions spec/timezone.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ <h1>Temporal.TimeZone ( _identifier_ )</h1>
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 × 10<sup>10</sup>)).
1. Else,
1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_identifier_).
1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception.
Expand Down Expand Up @@ -102,7 +102,7 @@ <h1>get Temporal.TimeZone.prototype.id</h1>
<emu-alg>
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 × 10<sup>10</sup>)).
1. Return _timeZone_.[[Identifier]].
</emu-alg>
</emu-clause>
Expand All @@ -116,7 +116,7 @@ <h1>Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( _instant_ )</h1>
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]])).
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -172,9 +172,9 @@ <h1>Temporal.TimeZone.prototype.getPossibleInstantsFor ( _dateTime_ )</h1>
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]] &times; (6 &times; 10<sup>10</sup>)) ».
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.
Expand All @@ -195,7 +195,7 @@ <h1>Temporal.TimeZone.prototype.getNextTransition ( _startingPoint_ )</h1>
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_).
Expand All @@ -211,7 +211,7 @@ <h1>Temporal.TimeZone.prototype.getPreviousTransition ( _startingPoint_ )</h1>
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_).
Expand All @@ -226,7 +226,7 @@ <h1>Temporal.TimeZone.prototype.toString ( )</h1>
<emu-alg>
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 × 10<sup>10</sup>)).
1. Return _timeZone_.[[Identifier]].
</emu-alg>
</emu-clause>
Expand All @@ -239,7 +239,7 @@ <h1>Temporal.TimeZone.prototype.toJSON ( )</h1>
<emu-alg>
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 × 10<sup>10</sup>)).
1. Return _timeZone_.[[Identifier]].
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -281,11 +281,11 @@ <h1>Properties of Temporal.TimeZone Instances</h1>
</tr>
<tr>
<td>
[[OffsetNanoseconds]]
[[OffsetMinutes]]
</td>
<td>
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 &minus;8.64 &times; 10<sup>13</sup> nanoseconds (&minus;24 hours) and smaller than &plus;8.64 &times; 10<sup>13</sup> nanoseconds (&plus;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 &minus;1440 minutes (&minus;24 hours) and smaller than &plus;1440 minutes (&plus;24 hours).
</td>
</tr>
</tbody>
Expand Down Expand Up @@ -322,18 +322,18 @@ <h1>
</dl>
<emu-alg>
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_.
</emu-alg>

Expand All @@ -344,7 +344,7 @@ <h1>
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.
</p>
<p>
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.
</p>
Expand Down Expand Up @@ -563,8 +563,8 @@ <h1>
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 × 10<sup>10</sup>)).
1. Let _timeZoneIdentifierRecord_ be GetAvailableNamedTimeZoneIdentifier(_name_).
1. If _timeZoneIdentifierRecord_ is ~empty~, throw a *RangeError* exception.
1. Return _timeZoneIdentifierRecord_.[[PrimaryIdentifier]].
Expand Down Expand Up @@ -815,13 +815,13 @@ <h1>
<h1>
ParseTimeZoneIdentifier (
_identifier_: a String
): a Record containing [[Name]] and [[OffsetNanoseconds]] fields, or ~unmatched~
): a Record containing [[Name]] and [[OffsetMinutes]] fields, or ~unmatched~
</h1>
<dl class="header">
<dt>description</dt>
<dd>
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.
</dd>
</dl>
Expand All @@ -830,13 +830,13 @@ <h1>
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 &times; 10<sup>10</sup> = 0.
1. Return the Record { [[Name]]: ~empty~, [[OffsetNanoseconds]]: _offsetNanoseconds_ }.
1. Return the Record { [[Name]]: ~empty~, [[OffsetMinutes]]: _offsetNanoseconds_ / (6 &times; 10<sup>10</sup>) }.
</emu-alg>
</emu-clause>
</emu-clause>
Expand Down

0 comments on commit f6818ce

Please sign in to comment.