Skip to content

Commit

Permalink
Limit offset time zones to minutes precision
Browse files Browse the repository at this point in the history
  • Loading branch information
justingrant committed Jun 15, 2023
1 parent 8b94fd6 commit 1dd7030
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 78 deletions.
62 changes: 42 additions & 20 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -361,20 +361,26 @@ export function RejectTemporalLikeObject(item) {
}

export function CanonicalizeTimeZoneOffsetString(offsetString) {
const offsetNs = ParseTimeZoneOffsetString(offsetString);
return FormatTimeZoneOffsetString(offsetNs);
const offsetMinutes = ParseTimeZoneOffsetStringMinutes(offsetString);
return FormatTimeZoneOffsetMinutes(offsetMinutes);
}

export function ParseTemporalTimeZone(stringIdent) {
const { tzName, offset, z } = ParseTemporalTimeZoneString(stringIdent);
if (tzName) {
if (IsTimeZoneOffsetString(tzName)) return CanonicalizeTimeZoneOffsetString(tzName);
if (IsTimeZoneOffsetStringNanosecondsPrecision(tzName)) {
throw new RangeError(`seconds not allowed in offset string: ${tzName}`);
}
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
if (!IsTimeZoneOffsetString(tzName)) {
throw new RangeError(`seconds not allowed in offset string: ${tzName}`);
}
return CanonicalizeTimeZoneOffsetString(offset);
}

Expand Down Expand Up @@ -641,7 +647,7 @@ export function ParseTemporalInstant(isoString) {
ParseTemporalInstantString(isoString);

if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ParseTimeZoneOffsetString(offset);
const offsetNs = z ? 0 : ParseTimeZoneOffsetStringNanoseconds(offset);
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime(
year,
month,
Expand Down Expand Up @@ -1005,7 +1011,7 @@ export function ToRelativeTemporalObject(options) {
calendar = ASCIILowercase(calendar);
}
if (timeZone === undefined) return CreateTemporalDate(year, month, day, calendar);
const offsetNs = offsetBehaviour === 'option' ? ParseTimeZoneOffsetString(offset) : 0;
const offsetNs = offsetBehaviour === 'option' ? ParseTimeZoneOffsetStringNanoseconds(offset) : 0;
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1406,7 +1412,7 @@ export function InterpretISODateTimeOffset(
// the user-provided offset doesn't match any instants for this time
// zone and date/time.
if (offsetOpt === 'reject') {
const offsetStr = FormatTimeZoneOffsetString(offsetNs);
const offsetStr = FormatTimeZoneOffsetNanoseconds(offsetNs);
const timeZoneString = IsTemporalTimeZone(timeZone) ? GetSlot(timeZone, TIMEZONE_ID) : 'time zone';
throw new RangeError(`Offset ${offsetStr} is invalid for ${dt} in ${timeZoneString}`);
}
Expand Down Expand Up @@ -1469,7 +1475,7 @@ export function ToTemporalZonedDateTime(item, options) {
ToTemporalOverflow(options); // validate and ignore
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetString(offset);
if (offsetBehaviour === 'option') offsetNs = ParseTimeZoneOffsetStringNanoseconds(offset);
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -2162,7 +2168,7 @@ export function GetOffsetNanosecondsFor(timeZone, instant, getOffsetNanosecondsF

export function GetOffsetStringFor(timeZone, instant) {
const offsetNs = GetOffsetNanosecondsFor(timeZone, instant);
return FormatTimeZoneOffsetString(offsetNs);
return FormatTimeZoneOffsetNanoseconds(offsetNs);
}

export function GetPlainDateTimeFor(timeZone, instant, calendar) {
Expand Down Expand Up @@ -2384,7 +2390,7 @@ export function TemporalInstantToString(instant, timeZone, precision) {
let timeZoneString = 'Z';
if (timeZone !== undefined) {
const offsetNs = GetOffsetNanosecondsFor(outputTimeZone, instant);
timeZoneString = FormatISOTimeZoneOffsetString(offsetNs);
timeZoneString = FormatTimeZoneOffsetRoundToMinutes(offsetNs);
}
return `${year}-${month}-${day}T${hour}:${minute}${seconds}${timeZoneString}`;
}
Expand Down Expand Up @@ -2564,7 +2570,7 @@ export function TemporalZonedDateTimeToString(
let result = `${year}-${month}-${day}T${hour}:${minute}${seconds}`;
if (showOffset !== 'never') {
const offsetNs = GetOffsetNanosecondsFor(tz, instant);
result += FormatISOTimeZoneOffsetString(offsetNs);
result += FormatTimeZoneOffsetRoundToMinutes(offsetNs);
}
if (showTimeZone !== 'never') {
const identifier = ToTemporalTimeZoneIdentifier(tz);
Expand All @@ -2579,7 +2585,26 @@ export function IsTimeZoneOffsetString(string) {
return OFFSET.test(string);
}

export function ParseTimeZoneOffsetString(string) {
export function IsTimeZoneOffsetStringNanosecondsPrecision(string) {
const match = OFFSET.exec(string);
return match && !match[4] && !match[5];
}

export function ParseTimeZoneOffsetStringMinutes(string) {
const match = OFFSET.exec(string);
if (!match) {
throw new RangeError(`invalid time zone offset: ${string}`);
}
if (match[4] || match[5]) {
throw new RangeError(`seconds are not allowed in time zone offset: ${string}`);
}
const sign = match[1] === '-' || match[1] === '\u2212' ? -1 : +1;
const hours = +match[2];
const minutes = +(match[3] || 0);
return sign * (hours * 60 + minutes);
}

export function ParseTimeZoneOffsetStringNanoseconds(string) {
const match = OFFSET.exec(string);
if (!match) {
throw new RangeError(`invalid time zone offset: ${string}`);
Expand Down Expand Up @@ -2702,7 +2727,7 @@ export function GetNamedTimeZoneOffsetNanoseconds(id, epochNanoseconds) {
return +utc.minus(epochNanoseconds);
}

export function FormatTimeZoneOffsetString(offsetNanoseconds) {
export function FormatTimeZoneOffsetNanoseconds(offsetNanoseconds) {
const sign = offsetNanoseconds < 0 ? '-' : '+';
offsetNanoseconds = MathAbs(offsetNanoseconds);
const nanoseconds = offsetNanoseconds % 1e9;
Expand All @@ -2724,16 +2749,13 @@ export function FormatTimeZoneOffsetString(offsetNanoseconds) {
return `${sign}${hourString}:${minuteString}${post}`;
}

export function FormatISOTimeZoneOffsetString(offsetNanoseconds) {
offsetNanoseconds = RoundNumberToIncrement(bigInt(offsetNanoseconds), 60e9, 'halfExpand').toJSNumber();
const sign = offsetNanoseconds < 0 ? '-' : '+';
offsetNanoseconds = MathAbs(offsetNanoseconds);
const minutes = (offsetNanoseconds / 60e9) % 60;
const hours = MathFloor(offsetNanoseconds / 3600e9);
export function FormatTimeZoneOffsetMinutes(offsetMinutes) {
return FormatTimeZoneOffsetNanoseconds(offsetMinutes * 6e10);
}

const hourString = ISODateTimePartString(hours);
const minuteString = ISODateTimePartString(minutes);
return `${sign}${hourString}:${minuteString}`;
export function FormatTimeZoneOffsetRoundToMinutes(offsetNanoseconds) {
offsetNanoseconds = RoundNumberToIncrement(bigInt(offsetNanoseconds), 60e9, 'halfExpand').toJSNumber();
return FormatTimeZoneOffsetNanoseconds(offsetNanoseconds);
}

export function GetUTCEpochNanoseconds(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond) {
Expand Down
4 changes: 2 additions & 2 deletions polyfill/lib/timezone.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class TimeZone {
const id = GetSlot(this, TIMEZONE_ID);

if (ES.IsTimeZoneOffsetString(id)) {
return ES.ParseTimeZoneOffsetString(id);
return ES.ParseTimeZoneOffsetStringMinutes(id) * 6e10;
}

return ES.GetNamedTimeZoneOffsetNanoseconds(id, GetSlot(instant, EPOCHNANOSECONDS));
Expand Down Expand Up @@ -98,7 +98,7 @@ export class TimeZone {
GetSlot(dateTime, ISO_NANOSECOND)
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
const offsetNs = ES.ParseTimeZoneOffsetString(id);
const offsetNs = ES.ParseTimeZoneOffsetStringMinutes(id) * 6e10;
return [new Instant(epochNs.minus(offsetNs))];
}

Expand Down
2 changes: 1 addition & 1 deletion polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ export class ZonedDateTime {

let { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, options);
const offsetNs = ES.ParseTimeZoneOffsetString(fields.offset);
const offsetNs = ES.ParseTimeZoneOffsetStringNanoseconds(fields.offset);
const timeZone = GetSlot(this, TIME_ZONE);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
Expand Down
36 changes: 24 additions & 12 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -622,8 +622,8 @@ <h1>ToRelativeTemporalObject ( _options_ )</h1>
1. If _timeZone_ is *undefined*, then
1. Return ? CreateTemporalDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _calendar_).
1. If _offsetBehaviour_ is ~option~, then
1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception.
1. Let _offsetNs_ be ParseTimeZoneOffsetString(_offsetString_).
1. If IsTimeZoneOffsetStringNanosecondPrecision(_offsetString_) is *false*, throw a *RangeError* exception.
1. Let _offsetNs_ be ParseTimeZoneOffsetStringNanoseconds(_offsetString_).
1. Else,
1. Let _offsetNs_ be 0.
1. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _offsetBehaviour_, _offsetNs_, _timeZone_, *"compatible"*, *"reject"*, _matchBehaviour_).
Expand Down Expand Up @@ -946,6 +946,7 @@ <h1>ISO 8601 grammar</h1>
<li>Alphabetic designators may be in lower or upper case.</li>
<li>Period or comma may be used as the decimal separator.</li>
<li>A time zone offset of *"-00:00"* is allowed, and means the same thing as *"+00:00"*.</li>
<li>UTC offsets may have seconds and up to 9 digits of sub-second decimals.</li>
<li>
In a combined representation, combinations of date, time, and time zone offset with Basic (no `-` or `:` separators) and Extended (with `-` or `:` separators) formatting are allowed.
(The date, time, and time zone offset must each be fully in Basic format or Extended format.)
Expand Down Expand Up @@ -1098,14 +1099,25 @@ <h1>ISO 8601 grammar</h1>
TimeFraction :
Fraction

TimeZoneUTCOffset :
UTCOffset
UTCOffsetMinutePrecision :
TimeZoneUTCOffsetName[+Extended]
TimeZoneUTCOffsetName[~Extended]

UTCOffsetWithSubMinute[Extended] :
Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction?

UTCOffsetNanosecondPrecision :
UTCDesignator
UTCOffsetMinutePrecision
UTCOffsetWithSubMinute[+Extended]
UTCOffsetWithSubMinute[~Extended]

NormalizedUTCOffset :
ASCIISign Hour[+Padded] `:` MinuteSecond

TimeZoneUTCOffsetName[Extended] :
Sign Hour[+Padded]
Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond
Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction?

TZLeadingChar :
Alpha
Expand Down Expand Up @@ -1186,21 +1198,21 @@ <h1>ISO 8601 grammar</h1>
TimeHour TimeMinute TimeSecond TimeFraction?

TimeSpecWithOptionalOffsetNotAmbiguous :
TimeSpec TimeZoneUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth
TimeSpec UTCOffsetNanosecondPrecision? but not one of ValidMonthDay or DateSpecYearMonth

DateTime :
Date
Date DateTimeSeparator TimeSpec TimeZoneUTCOffset?
Date DateTimeSeparator TimeSpec UTCOffsetNanosecondPrecision?

AnnotatedTime :
TimeDesignator TimeSpec TimeZoneUTCOffset? TimeZoneAnnotation? Annotations?
TimeDesignator TimeSpec UTCOffsetNanosecondPrecision? TimeZoneAnnotation? Annotations?
TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation? Annotations?

AnnotatedDateTime:
DateTime TimeZoneAnnotation? Annotations?

AnnotatedDateTimeTimeRequired :
Date DateTimeSeparator TimeSpec TimeZoneUTCOffset? TimeZoneAnnotation? Annotations?
Date DateTimeSeparator TimeSpec UTCOffsetNanosecondPrecision? TimeZoneAnnotation? Annotations?

AnnotatedYearMonth:
DateSpecYearMonth TimeZoneAnnotation? Annotations?
Expand Down Expand Up @@ -1281,7 +1293,7 @@ <h1>ISO 8601 grammar</h1>
Sign? DurationDesignator DurationTime

TemporalInstantString :
Date DateTimeSeparator TimeSpec TimeZoneUTCOffset TimeZoneAnnotation? Annotations?
Date DateTimeSeparator TimeSpec UTCOffsetNanosecondPrecision TimeZoneAnnotation? Annotations?

TemporalDateTimeString :
AnnotatedDateTime
Expand Down Expand Up @@ -1409,8 +1421,8 @@ <h1>
1. If _parseResult_ contains a |UTCDesignator| Parse Node, then
1. Set _timeZoneResult_.[[Z]] to *true*.
1. Else,
1. If _parseResult_ contains a |UTCOffset| Parse Node, then
1. Let _offset_ be the source text matched by the |UTCOffset| Parse Node contained within _parseResult_.
1. If _parseResult_ contains a |UTCOffsetNanosecondPrecision| Parse Node, then
1. Let _offset_ be the source text matched by the |UTCOffsetNanosecondPrecision| Parse Node contained within _parseResult_.
1. Set _timeZoneResult_.[[OffsetString]] to CodePointsToString(_offset_).
1. Let _calendar_ be *undefined*.
1. Let _calendarWasCritical_ be *false*.
Expand Down
6 changes: 3 additions & 3 deletions spec/instant.html
Original file line number Diff line number Diff line change
Expand Up @@ -532,8 +532,8 @@ <h1>ParseTemporalInstant ( _isoString_ )</h1>
1. Let _offsetString_ be _result_.[[TimeZoneOffsetString]].
1. Assert: _offsetString_ is not *undefined*.
1. Let _utc_ be GetUTCEpochNanoseconds(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]).
1. If IsTimeZoneOffsetString(_offsetString_) is *false*, throw a *RangeError* exception.
1. Let _offsetNanoseconds_ be ParseTimeZoneOffsetString(_offsetString_).
1. If IsTimeZoneOffsetStringNanosecondPrecision(_offsetString_) is *false*, throw a *RangeError* exception.
1. Let _offsetNanoseconds_ be ParseTimeZoneOffsetStringNanoseconds(_offsetString_).
1. Let _result_ be _utc_ - ℤ(_offsetNanoseconds_).
1. If ! IsValidEpochNanoseconds(_result_) is *false*, then
1. Throw a *RangeError* exception.
Expand Down Expand Up @@ -658,7 +658,7 @@ <h1>TemporalInstantToString ( _instant_, _timeZone_, _precision_ )</h1>
1. Let _timeZoneString_ be *"Z"*.
1. Else,
1. Let _offsetNs_ be ? GetOffsetNanosecondsFor(_timeZone_, _instant_).
1. Let _timeZoneString_ be ! FormatISOTimeZoneOffsetString(_offsetNs_).
1. Let _timeZoneString_ be FormatTimeZoneOffsetRoundToMinutes(_offsetNs_).
1. Return the string-concatenation of _dateTimeString_ and _timeZoneString_.
</emu-alg>
</emu-clause>
Expand Down
Loading

0 comments on commit 1dd7030

Please sign in to comment.