Skip to content

Editorial: Simpler time zone parsing in ToTemporalTimeZoneSlotValue #2641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 11, 2023
127 changes: 84 additions & 43 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ export function ParseISODateTime(isoString) {
} else if (match[14]) {
offset = match[14];
}
const tzName = match[15];
const tzAnnotation = match[15];
const calendar = processAnnotations(match[16]);
RejectDateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);
return {
Expand All @@ -454,7 +454,7 @@ export function ParseISODateTime(isoString) {
millisecond,
microsecond,
nanosecond,
tzName,
tzAnnotation,
offset,
z,
calendar
Expand All @@ -469,7 +469,7 @@ export function ParseTemporalInstantString(isoString) {

export function ParseTemporalZonedDateTimeString(isoString) {
const result = ParseISODateTime(isoString);
if (!result.tzName) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets');
if (!result.tzAnnotation) throw new RangeError('Temporal.ZonedDateTime requires a time zone ID in brackets');
return result;
}

Expand Down Expand Up @@ -562,29 +562,53 @@ export function ParseTemporalMonthDayString(isoString) {
const TIMEZONE_IDENTIFIER = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i');
const OFFSET_IDENTIFIER = new RegExp(`^${PARSE.offsetIdentifier.source}$`);

function throwBadTimeZoneStringError(timeZoneString) {
// Offset identifiers only support minute precision, but offsets in ISO
// strings support nanosecond precision. If the identifier is invalid but
// it's a valid ISO offset, then it has sub-minute precision. Show a clearer
// error message in that case.
const msg = OFFSET.test(timeZoneString) ? 'Seconds not allowed in offset time zone' : 'Invalid time zone';
throw new RangeError(`${msg}: ${timeZoneString}`);
}

export function ParseTimeZoneIdentifier(identifier) {
if (!TIMEZONE_IDENTIFIER.test(identifier)) throw new RangeError(`Invalid time zone identifier: ${identifier}`);
if (!TIMEZONE_IDENTIFIER.test(identifier)) {
throwBadTimeZoneStringError(identifier);
}
if (OFFSET_IDENTIFIER.test(identifier)) {
// The regex limits the input to minutes precision
const { offsetNanoseconds } = ParseDateTimeUTCOffset(identifier);
const offsetNanoseconds = ParseDateTimeUTCOffset(identifier);
// The regex limits the input to minutes precision, so we know that the
// division below will result in an integer.
return { offsetMinutes: offsetNanoseconds / 60e9 };
}
return { tzName: identifier };
}

export function ParseTemporalTimeZoneString(stringIdent) {
const bareID = new RegExp(`^${PARSE.timeZoneID.source}$`, 'i');
if (bareID.test(stringIdent)) return { tzName: stringIdent };
// This operation doesn't exist in the spec, but in the polyfill it's split from
// ParseTemporalTimeZoneString so that parsing can be tested separately from the
// logic of converting parsed values into a named or offset identifier.
export function ParseTemporalTimeZoneStringRaw(timeZoneString) {
if (TIMEZONE_IDENTIFIER.test(timeZoneString)) {
return { tzAnnotation: timeZoneString, offset: undefined, z: false };
}
try {
// Try parsing ISO string instead
const result = ParseISODateTime(stringIdent);
if (result.z || result.offset || result.tzName) {
return result;
const { tzAnnotation, offset, z } = ParseISODateTime(timeZoneString);
if (z || tzAnnotation || offset) {
return { tzAnnotation, offset, z };
}
} catch {
// fall through
}
throw new RangeError(`Invalid time zone: ${stringIdent}`);
throwBadTimeZoneStringError(timeZoneString);
}

export function ParseTemporalTimeZoneString(stringIdent) {
const { tzAnnotation, offset, z } = ParseTemporalTimeZoneStringRaw(stringIdent);
if (tzAnnotation) return ParseTimeZoneIdentifier(tzAnnotation);
if (z) return ParseTimeZoneIdentifier('UTC');
if (offset) return ParseTimeZoneIdentifier(offset);
throw new Error('this line should not be reached');
}

export function ParseTemporalDurationString(isoString) {
Expand Down Expand Up @@ -643,7 +667,7 @@ export function ParseTemporalInstant(isoString) {
ParseTemporalInstantString(isoString);

if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ParseDateTimeUTCOffset(offset).offsetNanoseconds;
const offsetNs = z ? 0 : ParseDateTimeUTCOffset(offset);
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = BalanceISODateTime(
year,
month,
Expand Down Expand Up @@ -986,11 +1010,24 @@ export function ToRelativeTemporalObject(options) {
timeZone = fields.timeZone;
if (timeZone !== undefined) timeZone = ToTemporalTimeZoneSlotValue(timeZone);
} else {
let tzName, z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, tzName, offset, z } =
ParseISODateTime(RequireString(relativeTo)));
if (tzName) {
timeZone = ToTemporalTimeZoneSlotValue(tzName);
let tzAnnotation, z;
({
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
calendar,
tzAnnotation,
offset,
z
} = ParseISODateTime(RequireString(relativeTo)));
if (tzAnnotation) {
timeZone = ToTemporalTimeZoneSlotValue(tzAnnotation);
if (z) {
offsetBehaviour = 'exact';
} else if (!offset) {
Expand All @@ -1007,7 +1044,7 @@ export function ToRelativeTemporalObject(options) {
calendar = ASCIILowercase(calendar);
}
if (timeZone === undefined) return CreateTemporalDate(year, month, day, calendar);
const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(offset).offsetNanoseconds : 0;
const offsetNs = offsetBehaviour === 'option' ? ParseDateTimeUTCOffset(offset) : 0;
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -1464,10 +1501,23 @@ export function ToTemporalZonedDateTime(item, options) {
options
));
} else {
let tzName, z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, tzName, offset, z, calendar } =
ParseTemporalZonedDateTimeString(RequireString(item)));
timeZone = ToTemporalTimeZoneSlotValue(tzName);
let tzAnnotation, z;
({
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
tzAnnotation,
offset,
z,
calendar
} = ParseTemporalZonedDateTimeString(RequireString(item)));
timeZone = ToTemporalTimeZoneSlotValue(tzAnnotation);
if (z) {
offsetBehaviour = 'exact';
} else if (!offset) {
Expand All @@ -1482,7 +1532,7 @@ export function ToTemporalZonedDateTime(item, options) {
ToTemporalOverflow(options); // validate and ignore
}
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(offset).offsetNanoseconds;
if (offsetBehaviour === 'option') offsetNs = ParseDateTimeUTCOffset(offset);
const epochNanoseconds = InterpretISODateTimeOffset(
year,
month,
Expand Down Expand Up @@ -2111,24 +2161,16 @@ export function ToTemporalTimeZoneSlotValue(temporalTimeZoneLike) {
}
return temporalTimeZoneLike;
}
const identifier = RequireString(temporalTimeZoneLike);
const { tzName, offset, z } = ParseTemporalTimeZoneString(identifier);
if (tzName) {
// tzName is any valid identifier string in brackets, and could be an offset identifier
const { offsetMinutes } = ParseTimeZoneIdentifier(tzName);
if (offsetMinutes !== undefined) return FormatOffsetTimeZoneIdentifier(offsetMinutes);
const timeZoneString = RequireString(temporalTimeZoneLike);

const record = GetAvailableNamedTimeZoneIdentifier(tzName);
if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`);
return record.identifier;
}
if (z) return 'UTC';
// if !tzName && !z then offset must be present
const { offsetNanoseconds, hasSubMinutePrecision } = ParseDateTimeUTCOffset(offset);
if (hasSubMinutePrecision) {
throw new RangeError(`Seconds not allowed in offset time zone: ${offset}`);
const { tzName, offsetMinutes } = ParseTemporalTimeZoneString(timeZoneString);
if (offsetMinutes !== undefined) {
return FormatOffsetTimeZoneIdentifier(offsetMinutes);
}
return FormatOffsetTimeZoneIdentifier(offsetNanoseconds / 60e9);
// if offsetMinutes is undefined, then tzName must be present
const record = GetAvailableNamedTimeZoneIdentifier(tzName);
if (!record) throw new RangeError(`Unrecognized time zone ${tzName}`);
return record.identifier;
}

export function ToTemporalTimeZoneIdentifier(slotValue) {
Expand Down Expand Up @@ -2620,8 +2662,7 @@ export function ParseDateTimeUTCOffset(string) {
const seconds = +(match[4] || 0);
const nanoseconds = +((match[5] || 0) + '000000000').slice(0, 9);
const offsetNanoseconds = sign * (((hours * 60 + minutes) * 60 + seconds) * 1e9 + nanoseconds);
const hasSubMinutePrecision = match[4] !== undefined || match[5] !== undefined;
return { offsetNanoseconds, hasSubMinutePrecision };
return offsetNanoseconds;
}

let canonicalTimeZoneIdsCache = undefined;
Expand Down
4 changes: 2 additions & 2 deletions polyfill/lib/regex.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const tzComponent = /\.[-A-Za-z_]|\.\.[-A-Za-z._]{1,12}|\.[-A-Za-z_][-A-Za-z._]{0,12}|[A-Za-z_][-A-Za-z._]{0,13}/;
const offsetNoCapture = /(?:[+\u2212-][0-2][0-9](?::?[0-5][0-9](?::?[0-5][0-9](?:[.,]\d{1,9})?)?)?)/;
const offsetIdentifierNoCapture = /(?:[+\u2212-][0-2][0-9](?::?[0-5][0-9])?)/;
export const timeZoneID = new RegExp(
'(?:' +
[
Expand All @@ -10,7 +10,7 @@ export const timeZoneID = new RegExp(
'CST6CDT',
'MST7MDT',
'PST8PDT',
offsetNoCapture.source
offsetIdentifierNoCapture.source
].join('|') +
')'
);
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.ParseDateTimeUTCOffset(fields.offset).offsetNanoseconds;
const offsetNs = ES.ParseDateTimeUTCOffset(fields.offset);
const timeZone = GetSlot(this, TIME_ZONE);
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
Expand Down
22 changes: 11 additions & 11 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const timeDesignator = character('Tt');
const weeksDesignator = character('Ww');
const yearsDesignator = character('Yy');
const utcDesignator = withCode(character('Zz'), (data) => {
data.z = 'Z';
data.z = true;
});
const annotationCriticalFlag = character('!');
const fraction = seq(decimalSeparator, between(1, 9, digit()));
Expand Down Expand Up @@ -266,7 +266,7 @@ const timeZoneUTCOffsetName = seq(sign, hour, choice([minuteSecond], seq(':', mi
const timeZoneIANAName = choice(...timezoneNames);
const timeZoneIdentifier = withCode(
choice(timeZoneUTCOffsetName, timeZoneIANAName),
(data, result) => (data.tzName = result)
(data, result) => (data.tzAnnotation = result)
);
const timeZoneAnnotation = seq('[', [annotationCriticalFlag], timeZoneIdentifier, ']');
const aKeyLeadingChar = choice(lcalpha(), character('_'));
Expand Down Expand Up @@ -426,7 +426,7 @@ const goals = {
const dateItems = ['year', 'month', 'day'];
const timeItems = ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'];
const comparisonItems = {
Instant: [...dateItems, ...timeItems, 'offset'],
Instant: [...dateItems, ...timeItems, 'offset', 'z'],
Date: [...dateItems, 'calendar'],
DateTime: [...dateItems, ...timeItems, 'calendar'],
Duration: [
Expand All @@ -443,9 +443,9 @@ const comparisonItems = {
],
MonthDay: ['month', 'day', 'calendar'],
Time: [...timeItems],
TimeZone: ['offset', 'tzName'],
TimeZone: ['offset', 'tzAnnotation', 'z'],
YearMonth: ['year', 'month', 'calendar'],
ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'tzName', 'calendar']
ZonedDateTime: [...dateItems, ...timeItems, 'offset', 'z', 'tzAnnotation', 'calendar']
};
const plainModes = ['Date', 'DateTime', 'MonthDay', 'Time', 'YearMonth'];

Expand All @@ -458,14 +458,14 @@ function fuzzMode(mode) {
fuzzed = goals[mode].generate(generatedData);
} while (plainModes.includes(mode) && /[0-9][zZ]/.test(fuzzed));
try {
const parsed = ES[`ParseTemporal${mode}String`](fuzzed);
const parsingMethod = ES[`ParseTemporal${mode}StringRaw`] ?? ES[`ParseTemporal${mode}String`];
const parsed = parsingMethod(fuzzed);
for (let prop of comparisonItems[mode]) {
let expected = generatedData[prop];
if (prop !== 'tzName' && prop !== 'offset' && prop !== 'calendar') expected = expected || 0;
if (prop === 'offset' && expected) {
const parsedResult = ES.ParseDateTimeUTCOffset(parsed[prop]);
assert.equal(parsedResult.offsetNanoseconds, expected.offsetNanoseconds);
assert.equal(parsedResult.hasSubMinutePrecision, expected.hasSubMinutePrecision);
if (!['tzAnnotation', 'offset', 'calendar'].includes(prop)) expected ??= prop === 'z' ? false : 0;
if (prop === 'offset') {
const parsedResult = parsed[prop] === undefined ? undefined : ES.ParseDateTimeUTCOffset(parsed[prop]);
assert.equal(parsedResult, expected, prop);
} else {
assert.equal(parsed[prop], expected, prop);
}
Expand Down
2 changes: 1 addition & 1 deletion polyfill/test262
Submodule test262 updated 34 files
+28 −2 test/built-ins/Temporal/Duration/compare/relativeto-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js
+50 −0 test/built-ins/Temporal/Instant/from/instant-string-sub-minute-offset.js
+50 −0 test/built-ins/Temporal/Instant/prototype/equals/instant-string-sub-minute-offset.js
+50 −0 test/built-ins/Temporal/Instant/prototype/since/instant-string-sub-minute-offset.js
+28 −2 test/built-ins/Temporal/Instant/prototype/toString/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Instant/prototype/toZonedDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Instant/prototype/toZonedDateTimeISO/timezone-string-datetime.js
+50 −0 test/built-ins/Temporal/Instant/prototype/until/instant-string-sub-minute-offset.js
+28 −2 test/built-ins/Temporal/Now/plainDate/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/plainDateISO/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/plainDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/plainDateTimeISO/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/plainTimeISO/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/zonedDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/Now/zonedDateTimeISO/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/PlainDate/prototype/toZonedDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/PlainDateTime/prototype/toZonedDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/PlainTime/prototype/toZonedDateTime/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/TimeZone/from/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/TimeZone/prototype/equals/timezone-string-datetime.js
+50 −0 test/built-ins/Temporal/TimeZone/prototype/getPlainDateTimeFor/instant-string-sub-minute-offset.js
+33 −3 test/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/ZonedDateTime/prototype/withTimeZone/timezone-string-datetime.js
+28 −2 test/built-ins/Temporal/ZonedDateTime/timezone-string-datetime.js
+74 −0 test/intl402/DisplayNames/prototype/of/type-language-valid.js
+28 −2 test/intl402/Temporal/Now/plainDateTimeISO/timezone-string-datetime.js
+0 −4 test/staging/Temporal/Regex/old/instant.js
23 changes: 15 additions & 8 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ <h1>ToRelativeTemporalObject (
1. If _timeZone_ is *undefined*, then
1. Return ? CreateTemporalDate(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _calendar_).
1. If _offsetBehaviour_ is ~option~, then
1. Let _offsetNs_ be ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]].
1. Let _offsetNs_ be ? ParseDateTimeUTCOffset(_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 @@ -1745,22 +1745,29 @@ <h1>
It parses the argument as either a time zone identifier or an ISO 8601 string.
The returned Record's fields are set as follows:
<ul>
<li>If _timeZoneString_ is either a named time zone identifier or offset time zone identifier, then [[Name]] is _timeZoneString_, while [[Z]] is *false* and [[OffsetString]] is *undefined*.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string with a time zone annotation containing either a named time zone identifier or offset time zone identifier, then [[Name]] is the time zone identifier contained in the annotation, while [[Z]] is *false* and [[OffsetString]] is *undefined*.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string using a *Z* offset designator, then [[Z]] is *true*, while [[Name]] and [[OffsetString]] are *undefined*.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string using a numeric UTC offset, then [[OffsetString]] is _timeZoneString_, while [[Z]] is *false* and [[Name]] is *undefined*.</li>
<li>If _timeZoneString_ is a named time zone identifier, then [[Name]] is _timeZoneString_ and [[OffsetMinutes]] is ~empty~.</li>
<li>Otherwise, if _timeZoneString_ is an offset time zone identifier, then [[OffsetMinutes]] is a signed integer and [[Name]] is ~empty~.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string with a time zone annotation containing a named time zone identifier, then [[Name]] is the time zone identifier contained in the annotation and [[OffsetMinutes]] is ~empty~.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string with a time zone annotation containing an offset time zone identifier, then [[OffsetMinutes]] is a signed integer and [[Name]] is ~empty~.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string using a *Z* offset designator, then [[Name]] is *"UTC"* and [[OffsetMinutes]] is ~empty~.</li>
<li>Otherwise, if _timeZoneString_ is an ISO 8601 string using a numeric UTC offset, then [[OffsetMinutes]] is a signed integer and [[Name]] is ~empty~.</li>
<li>Otherwise, a *RangeError* is thrown.</li>
</ul>
</dd>
</dl>
<emu-alg>
1. Let _parseResult_ be ParseText(StringToCodePoints(_timeZoneString_), |TimeZoneIdentifier|).
1. If _parseResult_ is a Parse Node, then
1. Return the Record { [[Z]]: *false*, [[OffsetString]]: *undefined*, [[Name]]: _timeZoneString_ }.
1. Return ! ParseTimeZoneIdentifier(_timeZoneString_).
1. Let _result_ be ? ParseISODateTime(_timeZoneString_).
1. Let _timeZoneResult_ be _result_.[[TimeZone]].
1. If _timeZoneResult_.[[Z]] is *false*, _timeZoneResult_.[[OffsetString]] is *undefined*, and _timeZoneResult_.[[Name]] is *undefined*, throw a *RangeError* exception.
1. Return _timeZoneResult_.
1. If _timeZoneResult_.[[Name]] is not *undefined*, then
1. Return ! ParseTimeZoneIdentifier(_timeZoneResult_.[[Name]]).
1. If _timeZoneResult_.[[Z]] is *true*, then
1. Return ! ParseTimeZoneIdentifier(*"UTC"*).
1. If _timeZoneResult_.[[OffsetString]] is not *undefined*, then
1. Return ? ParseTimeZoneIdentifier(_timeZoneResult_.[[OffsetString]]).
1. Throw a *RangeError* exception.
</emu-alg>
</emu-clause>

Expand Down
2 changes: 1 addition & 1 deletion spec/instant.html
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ <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. Let _offsetNanoseconds_ be ? ParseDateTimeUTCOffset(_offsetString_).[[OffsetNanoseconds]].
1. Let _offsetNanoseconds_ be ? ParseDateTimeUTCOffset(_offsetString_).
1. Let _result_ be _utc_ - ℤ(_offsetNanoseconds_).
1. If ! IsValidEpochNanoseconds(_result_) is *false*, then
1. Throw a *RangeError* exception.
Expand Down
Loading