Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 43 additions & 20 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,11 @@ export const ES = ObjectAssign({}, ES2020, {
IsTemporalZonedDateTime: (item) => HasSlot(item, EPOCHNANOSECONDS, TIME_ZONE, CALENDAR),
TemporalTimeZoneFromString: (stringIdent) => {
let { ianaName, offset, z } = ES.ParseTemporalTimeZoneString(stringIdent);
if (z) ianaName = 'UTC';
const result = ES.GetCanonicalTimeZoneIdentifier(ianaName || offset);
if (offset && ianaName && ianaName !== offset) {
let identifier = ianaName;
if (!identifier && z) identifier = 'UTC';
if (!identifier) identifier = offset;
const result = ES.GetCanonicalTimeZoneIdentifier(identifier);
if (offset && identifier !== offset) {
const ns = ES.ParseTemporalInstant(stringIdent);
const offsetNs = ES.GetIANATimeZoneOffsetNanoseconds(ns, result);
if (ES.FormatTimeZoneOffsetString(offsetNs) !== offset) {
Expand Down Expand Up @@ -245,10 +247,11 @@ export const ES = ObjectAssign({}, ES2020, {
const millisecond = ES.ToInteger(fraction.slice(0, 3));
const microsecond = ES.ToInteger(fraction.slice(3, 6));
const nanosecond = ES.ToInteger(fraction.slice(6, 9));
let offset, z;
let offset;
let z = false;
if (match[13]) {
offset = '+00:00';
z = 'Z';
offset = undefined;
z = true;
} else if (match[14] && match[15]) {
const offsetSign = match[14] === '-' || match[14] === '\u2212' ? '-' : '+';
const offsetHours = match[15] || '00';
Expand Down Expand Up @@ -401,7 +404,7 @@ export const ES = ObjectAssign({}, ES2020, {
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
},
ParseTemporalInstant: (isoString) => {
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offset } =
const { year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, offset, z } =
ES.ParseTemporalInstantString(isoString);

const epochNs = ES.GetEpochFromISOParts(
Expand All @@ -416,8 +419,8 @@ export const ES = ObjectAssign({}, ES2020, {
nanosecond
);
if (epochNs === null) throw new RangeError('DateTime outside of supported range');
if (!offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = ES.ParseOffsetString(offset);
if (!z && !offset) throw new RangeError('Temporal.Instant requires a time zone offset');
const offsetNs = z ? 0 : ES.ParseOffsetString(offset);
return epochNs.subtract(offsetNs);
},
RegulateISODateTime: (year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, overflow) => {
Expand Down Expand Up @@ -728,6 +731,7 @@ export const ES = ObjectAssign({}, ES2020, {
const relativeTo = options.relativeTo;
if (relativeTo === undefined) return relativeTo;

let offsetBehaviour = 'option';
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, timeZone, offset;
if (ES.Type(relativeTo) === 'Object') {
if (ES.IsTemporalZonedDateTime(relativeTo) || ES.IsTemporalDateTime(relativeTo)) return relativeTo;
Expand All @@ -753,19 +757,25 @@ export const ES = ObjectAssign({}, ES2020, {
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } =
ES.InterpretTemporalDateTimeFields(calendar, fields, dateOptions));
offset = relativeTo.offset;
if (offset === undefined) offsetBehaviour = 'wall';
timeZone = relativeTo.timeZone;
} else {
let ianaName;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, ianaName, offset } =
let ianaName, z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, calendar, ianaName, offset, z } =
ES.ParseISODateTime(ES.ToString(relativeTo), { zoneRequired: false }));
if (ianaName) timeZone = ianaName;
if (z) {
offsetBehaviour = 'exact';
} else if (!offset) {
offsetBehaviour = 'wall';
}
if (!calendar) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
}
if (timeZone) {
timeZone = ES.ToTemporalTimeZone(timeZone);
let offsetNs = null;
if (offset) offsetNs = ES.ParseOffsetString(ES.ToString(offset));
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(ES.ToString(offset));
const epochNanoseconds = ES.InterpretISODateTimeOffset(
year,
month,
Expand All @@ -776,6 +786,7 @@ export const ES = ObjectAssign({}, ES2020, {
millisecond,
microsecond,
nanosecond,
offsetBehaviour,
offsetNs,
timeZone,
'compatible',
Expand Down Expand Up @@ -1246,6 +1257,7 @@ export const ES = ObjectAssign({}, ES2020, {
millisecond,
microsecond,
nanosecond,
offsetBehaviour,
offsetNs,
timeZone,
disambiguation,
Expand All @@ -1254,7 +1266,7 @@ export const ES = ObjectAssign({}, ES2020, {
const DateTime = GetIntrinsic('%Temporal.PlainDateTime%');
const dt = new DateTime(year, month, day, hour, minute, second, millisecond, microsecond, nanosecond);

if (offsetNs === null || offsetOpt === 'ignore') {
if (offsetBehaviour === 'wall' || offsetOpt === 'ignore') {
// Simple case: ISO string without a TZ offset (or caller wants to ignore
// the offset), so just convert DateTime to Instant in the given time zone
const instant = ES.BuiltinTimeZoneGetInstantFor(timeZone, dt, disambiguation);
Expand All @@ -1264,7 +1276,7 @@ export const ES = ObjectAssign({}, ES2020, {
// The caller wants the offset to always win ('use') OR the caller is OK
// with the offset winning ('prefer' or 'reject') as long as it's valid
// for this timezone and date/time.
if (offsetOpt === 'use') {
if (offsetBehaviour === 'exact' || offsetOpt === 'use') {
// Calculate the instant for the input's date/time and offset
const epochNs = ES.GetEpochFromISOParts(
year,
Expand Down Expand Up @@ -1302,6 +1314,7 @@ export const ES = ObjectAssign({}, ES2020, {
},
ToTemporalZonedDateTime: (item, options = ObjectCreate(null)) => {
let year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, timeZone, offset, calendar;
let offsetBehaviour = 'option';
if (ES.Type(item) === 'Object') {
if (ES.IsTemporalZonedDateTime(item)) return item;
calendar = ES.GetTemporalCalendarWithISODefault(item);
Expand All @@ -1322,20 +1335,29 @@ export const ES = ObjectAssign({}, ES2020, {
ES.InterpretTemporalDateTimeFields(calendar, fields, options));
timeZone = ES.ToTemporalTimeZone(fields.timeZone);
offset = fields.offset;
if (offset !== undefined) offset = ES.ToString(offset);
if (offset === undefined) {
offsetBehaviour = 'wall';
} else {
offset = ES.ToString(offset);
}
} else {
ES.ToTemporalOverflow(options); // validate and ignore
let ianaName;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, ianaName, offset, calendar } =
let ianaName, z;
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond, ianaName, offset, z, calendar } =
ES.ParseTemporalZonedDateTimeString(ES.ToString(item)));
if (!ianaName) throw new RangeError('time zone ID required in brackets');
if (z) {
offsetBehaviour = 'exact';
} else if (!offset) {
offsetBehaviour = 'wall';
}
const TemporalTimeZone = GetIntrinsic('%Temporal.TimeZone%');
timeZone = new TemporalTimeZone(ianaName);
if (!calendar) calendar = ES.GetISO8601Calendar();
calendar = ES.ToTemporalCalendar(calendar);
}
let offsetNs = null;
if (offset) offsetNs = ES.ParseOffsetString(offset);
let offsetNs = 0;
if (offsetBehaviour === 'option') offsetNs = ES.ParseOffsetString(offset);
const disambiguation = ES.ToTemporalDisambiguation(options);
const offsetOpt = ES.ToTemporalOffset(options, 'reject');
const epochNanoseconds = ES.InterpretISODateTimeOffset(
Expand All @@ -1348,6 +1370,7 @@ export const ES = ObjectAssign({}, ES2020, {
millisecond,
microsecond,
nanosecond,
offsetBehaviour,
offsetNs,
timeZone,
disambiguation,
Expand Down
2 changes: 2 additions & 0 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export class ZonedDateTime {
millisecond,
microsecond,
nanosecond,
'option',
offsetNs,
timeZone,
disambiguation,
Expand Down Expand Up @@ -632,6 +633,7 @@ export class ZonedDateTime {
millisecond,
microsecond,
nanosecond,
'option',
offsetNs,
timeZone,
'compatible',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (C) 2021 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.compare
description: >
Conversion of ISO date-time strings as relativeTo option to
Temporal.ZonedDateTime or Temporal.PlainDateTime instances
features: [Temporal]
---*/

const instance = new Temporal.Duration(1, 0, 0, 1);
const equalIfPlain = new Temporal.Duration(1, 0, 0, 0, 24);
const equalIfZoned = new Temporal.Duration(1, 0, 0, 0, 25);

let relativeTo = "2019-11-01T00:00";
const result1 = Temporal.Duration.compare(instance, equalIfPlain, { relativeTo });
assert.sameValue(result1, 0, "bare date-time string is a plain relativeTo");

relativeTo = "2019-11-01T00:00Z";
const result2 = Temporal.Duration.compare(instance, equalIfPlain, { relativeTo });
assert.sameValue(result2, 0, "date-time + Z is a plain relativeTo");

relativeTo = "2019-11-01T00:00-07:00";
const result3 = Temporal.Duration.compare(instance, equalIfPlain, { relativeTo });
assert.sameValue(result3, 0, "date-time + offset is a plain relativeTo");

relativeTo = "2019-11-01T00:00[America/Vancouver]";
const result4 = Temporal.Duration.compare(instance, equalIfZoned, { relativeTo });
assert.sameValue(result4, 0, "date-time + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00Z[America/Vancouver]";
const result5 = Temporal.Duration.compare(instance, equalIfZoned, { relativeTo });
assert.sameValue(result5, 0, "date-time + Z + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00-07:00[America/Vancouver]";
const result6 = Temporal.Duration.compare(instance, equalIfZoned, { relativeTo });
assert.sameValue(result6, 0, "date-time + offset + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00+04:15[America/Vancouver]";
assert.throws(RangeError, () => Temporal.Duration.compare(instance, equalIfPlain, { relativeTo }), "date-time + offset + IANA annotation throws if wall time and exact time mismatch");

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (C) 2021 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.compare
description: Conversion of ISO date-time strings to Temporal.TimeZone instances
features: [Temporal]
---*/

let timeZone = "2021-08-19T17:30";
assert.throws(RangeError, () => Temporal.Duration.compare(new Temporal.Duration(), new Temporal.Duration(), { relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "bare date-time string is not a time zone");
assert.throws(RangeError, () => Temporal.Duration.compare(new Temporal.Duration(), new Temporal.Duration(), { relativeTo: { year: 2000, month: 5, day: 2, timeZone: { timeZone } } }), "bare date-time string is not a time zone");

// The following are all valid strings so should not throw:

[
"2021-08-19T17:30Z",
"2021-08-19T17:30-07:00",
"2021-08-19T17:30[America/Vancouver]",
"2021-08-19T17:30Z[America/Vancouver]",
"2021-08-19T17:30-07:00[America/Vancouver]",
].forEach((timeZone) => {
Temporal.Duration.compare(new Temporal.Duration(), new Temporal.Duration(), { relativeTo: { year: 2000, month: 5, day: 2, timeZone } });
Temporal.Duration.compare(new Temporal.Duration(), new Temporal.Duration(), { relativeTo: { year: 2000, month: 5, day: 2, timeZone: { timeZone } } });
});
40 changes: 40 additions & 0 deletions polyfill/test/Duration/prototype/add/relativeto-string-datetime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (C) 2021 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.add
description: >
Conversion of ISO date-time strings as relativeTo option to
Temporal.ZonedDateTime or Temporal.PlainDateTime instances
includes: [temporalHelpers.js]
features: [Temporal]
---*/

const instance = new Temporal.Duration(1, 0, 0, 1);

let relativeTo = "2019-11-01T00:00";
const result1 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, "bare date-time string is a plain relativeTo");

relativeTo = "2019-11-01T00:00Z";
const result2 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, "date-time + Z is a plain relativeTo");

relativeTo = "2019-11-01T00:00-07:00";
const result3 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result3, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, "date-time + offset is a plain relativeTo");

relativeTo = "2019-11-01T00:00[America/Vancouver]";
const result4 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result4, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, "date-time + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00Z[America/Vancouver]";
const result5 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result5, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, "date-time + Z + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00-07:00[America/Vancouver]";
const result6 = instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo });
TemporalHelpers.assertDuration(result6, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, "date-time + offset + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00+04:15[America/Vancouver]";
assert.throws(RangeError, () => instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo }), "date-time + offset + IANA annotation throws if wall time and exact time mismatch");

This file was deleted.

27 changes: 27 additions & 0 deletions polyfill/test/Duration/prototype/add/timezone-string-datetime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (C) 2021 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.add
description: Conversion of ISO date-time strings to Temporal.TimeZone instances
features: [Temporal]
---*/

const instance = new Temporal.Duration(1);

let timeZone = "2021-08-19T17:30";
assert.throws(RangeError, () => instance.add(new Temporal.Duration(1), { relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "bare date-time string is not a time zone");
assert.throws(RangeError, () => instance.add(new Temporal.Duration(1), { relativeTo: { year: 2000, month: 5, day: 2, timeZone: { timeZone } } }), "bare date-time string is not a time zone");

// The following are all valid strings so should not throw:

[
"2021-08-19T17:30Z",
"2021-08-19T17:30-07:00",
"2021-08-19T17:30[America/Vancouver]",
"2021-08-19T17:30Z[America/Vancouver]",
"2021-08-19T17:30-07:00[America/Vancouver]",
].forEach((timeZone) => {
instance.add(new Temporal.Duration(1), { relativeTo: { year: 2000, month: 5, day: 2, timeZone } });
instance.add(new Temporal.Duration(1), { relativeTo: { year: 2000, month: 5, day: 2, timeZone: { timeZone } } });
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (C) 2021 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.

/*---
esid: sec-temporal.duration.prototype.round
description: >
Conversion of ISO date-time strings as relativeTo option to
Temporal.ZonedDateTime or Temporal.PlainDateTime instances
includes: [temporalHelpers.js]
features: [Temporal]
---*/

const instance = new Temporal.Duration(1, 0, 0, 0, 24);

let relativeTo = "2019-11-01T00:00";
const result1 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, "bare date-time string is a plain relativeTo");

relativeTo = "2019-11-01T00:00Z";
const result2 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, "date-time + Z is a plain relativeTo");

relativeTo = "2019-11-01T00:00-07:00";
const result3 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result3, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, "date-time + offset is a plain relativeTo");

relativeTo = "2019-11-01T00:00[America/Vancouver]";
const result4 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result4, 1, 0, 0, 0, 24, 0, 0, 0, 0, 0, "date-time + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00Z[America/Vancouver]";
const result5 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result5, 1, 0, 0, 0, 24, 0, 0, 0, 0, 0, "date-time + Z + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00-07:00[America/Vancouver]";
const result6 = instance.round({ largestUnit: "years", relativeTo });
TemporalHelpers.assertDuration(result6, 1, 0, 0, 0, 24, 0, 0, 0, 0, 0, "date-time + offset + IANA annotation is a zoned relativeTo");

relativeTo = "2019-11-01T00:00+04:15[America/Vancouver]";
assert.throws(RangeError, () => instance.round({ largestUnit: "years", relativeTo }), "date-time + offset + IANA annotation throws if wall time and exact time mismatch");
Loading