summaryrefslogtreecommitdiffstats
path: root/js/src/tests/test262/built-ins/Temporal/ZonedDateTime
diff options
context:
space:
mode:
Diffstat (limited to 'js/src/tests/test262/built-ins/Temporal/ZonedDateTime')
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-calendar-iso-string.js30
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-backward-offset-shift.js51
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-forward-offset-shift.js46
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-string-calendar-annotation-invalid-key.js32
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-iso-string.js28
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-year-zero.js3
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-forward-offset-shift.js44
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-string-calendar-annotation-invalid-key.js32
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-iso-string.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-year-zero.js3
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-backward-offset-shift.js50
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-forward-offset-shift.js45
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-string-calendar-annotation-invalid-key.js25
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js44
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-maximum-backward-offset-shift.js53
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/div-zero.js8
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getoffsetnanosecondsfor-maximum-forward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-called-with-iso8601-calendar.js4
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-maximum-backward-offset-shift.js53
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/order-of-operations.js42
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/smallest-unit-day-rounding-modes.js47
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/timezone-getpossibleinstantsfor-iterable.js2
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-iso-string.js30
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-year-zero.js3
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-backward-offset-shift.js50
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-forward-offset-shift.js45
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-string-calendar-annotation-invalid-key.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/calendar-dateadd-called-with-options-undefined.js16
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/differencezoneddatetime-inconsistent-custom-calendar.js56
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-month-day-boundary.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js48
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js84
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-loop-arbitrarily.js86
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-range-errors.js51
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/order-of-operations.js15
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/wrapping-at-end-of-month.js120
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js44
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-maximum-backward-offset-shift.js53
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-iso-string.js30
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-year-zero.js3
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-backward-offset-shift.js50
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-forward-offset-shift.js45
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-string-calendar-annotation-invalid-key.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/calendar-dateadd-called-with-options-undefined.js16
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/differencezoneddatetime-inconsistent-custom-calendar.js56
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-month-day-boundary.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js48
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js85
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-loop-arbitrarily.js86
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-range-errors.js51
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/order-of-operations.js15
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/wrapping-at-end-of-month.js120
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/custom-calendar-weekofyear.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/validate-calendar-value.js1
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-maximum-forward-offset-shift.js52
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js46
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-maximum-backward-offset-shift.js56
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-out-of-range-backward-offset-shift.js51
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withCalendar/calendar-iso-string.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-iso-string.js29
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-year-zero.js3
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-string-calendar-annotation-invalid-key.js26
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-maximum-forward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js44
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-maximum-backward-offset-shift.js53
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/argument-string-calendar-annotation-invalid-key.js32
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-maximum-forward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js44
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-maximum-backward-offset-shift.js53
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-out-of-range-backward-offset-shift.js49
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/custom-calendar-weekofyear.js27
-rw-r--r--js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/validate-calendar-value.js1
79 files changed, 2934 insertions, 286 deletions
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..3a440b4994
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,30 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.compare
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+features: [Temporal]
+---*/
+
+const datetime = new Temporal.ZonedDateTime(0n, "UTC");
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const arg = { year: 1970, monthCode: "M01", day: 1, timeZone: "UTC", calendar };
+ const result1 = Temporal.ZonedDateTime.compare(arg, datetime);
+ assert.sameValue(result1, 0, `Calendar created from string "${calendar}" (first argument)`);
+ const result2 = Temporal.ZonedDateTime.compare(datetime, arg);
+ assert.sameValue(result2, 0, `Calendar created from string "${calendar}" (second argument)`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..9e09864074
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-backward-offset-shift.js
@@ -0,0 +1,51 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.compare
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+const datetime = new Temporal.ZonedDateTime(0n, timeZone);
+
+assert.throws(RangeError, () => Temporal.ZonedDateTime.compare(arg, datetime), "RangeError should be thrown (first argument)");
+assert.throws(RangeError, () => Temporal.ZonedDateTime.compare(datetime, arg), "RangeError should be thrown (second argument)");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..d2fe4ab99f
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-propertybag-out-of-range-forward-offset-shift.js
@@ -0,0 +1,46 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.compare
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+const datetime = new Temporal.ZonedDateTime(0n, timeZone);
+
+assert.throws(RangeError, () => Temporal.ZonedDateTime.compare(arg, datetime), "RangeError should be thrown (first argument)");
+assert.throws(RangeError, () => Temporal.ZonedDateTime.compare(datetime, arg), "RangeError should be thrown (second argument)");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..a3f48aa25a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/compare/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,32 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.compare
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const datetime = new Temporal.ZonedDateTime(0n, "UTC");
+
+const invalidStrings = [
+ ["1970-01-01T00:00[UTC][U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01T00:00[UTC][u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01T00:00[UTC][FOO=bar]", "invalid capitalized unrecognized key"],
+];
+
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => Temporal.ZonedDateTime.compare(arg, datetime),
+ `annotation keys must be lowercase: ${arg} - ${descr} (first argument)`
+ );
+ assert.throws(
+ RangeError,
+ () => Temporal.ZonedDateTime.compare(datetime, arg),
+ `annotation keys must be lowercase: ${arg} - ${descr} (second argument)`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..ea5074b440
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,28 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.from
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+features: [Temporal]
+---*/
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const timeZone = new Temporal.TimeZone("UTC");
+const arg = { year: 1970, monthCode: "M01", day: 1, timeZone, calendar };
+ const result = Temporal.ZonedDateTime.from(arg);
+ assert.sameValue(result.calendarId, "iso8601", `Calendar created from string "${calendar}"`);
+ assert.sameValue(result.getISOFields().calendar, "iso8601", "calendar slot stores a string");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-year-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-year-zero.js
index d736edeb21..2741d61844 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-year-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-calendar-year-zero.js
@@ -16,7 +16,8 @@ const invalidStrings = [
"-000000-10-31T17:45+00:00[UTC]",
];
-invalidStrings.forEach((arg) => {
+invalidStrings.forEach((str) => {
+ const arg = { year: 1976, month: 11, day: 18, calendar: str };
assert.throws(
RangeError,
() => Temporal.ZonedDateTime.from(arg),
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..1c4da44c1b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.from
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+assert.throws(RangeError, () => Temporal.ZonedDateTime.from(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..fad0037aae
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-propertybag-out-of-range-forward-offset-shift.js
@@ -0,0 +1,44 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.from
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+assert.throws(RangeError, () => Temporal.ZonedDateTime.from(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..1ea7b9aea4
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/from/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,32 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.from
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["1970-01-01T00:00[UTC][U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01T00:00[UTC][u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01T00:00[UTC][FOO=bar]", "invalid capitalized unrecognized key"],
+];
+
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => Temporal.ZonedDateTime.from(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+ for (const offset of ["use", "prefer", "ignore", "reject"]) {
+ assert.throws(
+ RangeError,
+ () => Temporal.ZonedDateTime.from(arg, { offset }),
+ `annotation keys must be lowercase: ${arg} - ${descr} (offset ${offset})`
+ );
+ }
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..4ee800c7bc
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.equals
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+features: [Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const arg = { year: 1970, monthCode: "M01", day: 1, timeZone, calendar };
+ const result = instance.equals(arg);
+ assert.sameValue(result, true, `Calendar created from string "${calendar}"`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-year-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-year-zero.js
index c0230f2f3e..196e992b08 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-year-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-calendar-year-zero.js
@@ -17,7 +17,8 @@ const invalidStrings = [
];
const timeZone = new Temporal.TimeZone("UTC");
const instance = new Temporal.ZonedDateTime(0n, timeZone);
-invalidStrings.forEach((arg) => {
+invalidStrings.forEach((str) => {
+ const arg = { year: 1976, month: 11, day: 18, calendar: str };
assert.throws(
RangeError,
() => instance.equals(arg),
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..bf311b856d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-backward-offset-shift.js
@@ -0,0 +1,50 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.equals
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.equals(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..8b27e78eff
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-propertybag-out-of-range-forward-offset-shift.js
@@ -0,0 +1,45 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.equals
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.equals(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..89b0d4d509
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/equals/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,25 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.equals
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["1970-01-01T00:00[UTC][U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01T00:00[UTC][u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01T00:00[UTC][FOO=bar]", "invalid capitalized unrecognized key"],
+];
+const instance = new Temporal.ZonedDateTime(0n, "UTC");
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => instance.equals(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..f41e0be158
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.hoursinday
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.hoursInDay;
+
+assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..7a41b9456e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
@@ -0,0 +1,44 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.hoursinday
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.hoursInDay, "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..8312ef765b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,53 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.hoursinday
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.hoursInDay;
+
+assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..566b61b2b0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.hoursinday
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.hoursInDay, "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/div-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/div-zero.js
index 776cb7454f..dd912f46d4 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/div-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/div-zero.js
@@ -18,10 +18,8 @@ class TimeZone extends Temporal.TimeZone {
}
}
-const units = ["hour", "minute", "second", "millisecond", "microsecond", "nanosecond"];
-for (const smallestUnit of units) {
- const zdt = new Temporal.ZonedDateTime(0n, new TimeZone("UTC"));
- assert.throws(RangeError, () => zdt.round({ smallestUnit, roundingIncrement: 2 }), `zero day-length with smallestUnit ${smallestUnit}`);
-}
+const zdt = new Temporal.ZonedDateTime(0n, new TimeZone("UTC"));
+
+assert.throws(RangeError, () => zdt.round({ smallestUnit: "day" }), `zero day-length with smallestUnit 'day'`);
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..1d3a5425d0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.round
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.round({ smallestUnit: "hours" });
+
+assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-called-with-iso8601-calendar.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-called-with-iso8601-calendar.js
index 0c622df167..91c8ec7bc2 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-called-with-iso8601-calendar.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-called-with-iso8601-calendar.js
@@ -51,8 +51,8 @@ const nonBuiltinISOCalendar = new Temporal.Calendar("iso8601");
const timeZone = new SkippedDateTime();
const instance = new Temporal.ZonedDateTime(0n, timeZone, nonBuiltinISOCalendar);
-instance.round({ smallestUnit: "hours" });
+instance.round({ smallestUnit: "day" });
-assert.sameValue(timeZone.calls, 6, "getPossibleInstantsFor should have been called 6 times");
+assert.sameValue(timeZone.calls, 4, "getPossibleInstantsFor should have been called 4 times");
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..0c60d44311
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,53 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.round
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.round({ smallestUnit: "hours" });
+
+assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..dabcfe8ed6
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.round
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.round({ smallestUnit: "hours" }), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/order-of-operations.js
index 48ca638b39..65fac9036c 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/order-of-operations.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/order-of-operations.js
@@ -24,10 +24,6 @@ const expected = [
"get this.timeZone.getPossibleInstantsFor",
// GetPlainDateTimeFor on receiver's instant
"call this.timeZone.getOffsetNanosecondsFor",
- // GetInstantFor on preceding midnight
- "call this.timeZone.getPossibleInstantsFor",
- // AddDaysToZonedDateTime
- "call this.timeZone.getPossibleInstantsFor",
// InterpretISODateTimeOffset
"call this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getOffsetNanosecondsFor",
@@ -84,36 +80,8 @@ beforeFallBackInstance.round(nextHourOptions);
assert.compareArray(actual, expected, "order of operations with rounding result at repeated wall-clock time");
actual.splice(0); // clear
-const expectedSkippedDateTime = [
- "get options.roundingIncrement",
- "get options.roundingIncrement.valueOf",
- "call options.roundingIncrement.valueOf",
- "get options.roundingMode",
- "get options.roundingMode.toString",
- "call options.roundingMode.toString",
- "get options.smallestUnit",
- "get options.smallestUnit.toString",
- "call options.smallestUnit.toString",
- // lookup
- "get this.timeZone.getOffsetNanosecondsFor",
- "get this.timeZone.getPossibleInstantsFor",
- // GetPlainDateTimeFor on receiver's instant
- "call this.timeZone.getOffsetNanosecondsFor",
- // GetInstantFor on preceding midnight
- "call this.timeZone.getPossibleInstantsFor",
- // DisambiguatePossibleInstants
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getPossibleInstantsFor",
- // AddZonedDateTime
- "call this.timeZone.getPossibleInstantsFor",
- // InterpretISODateTimeOffset
- "call this.timeZone.getPossibleInstantsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
-];
-
springForwardInstance.round(options);
-assert.compareArray(actual, expectedSkippedDateTime, "order of operations with preceding midnight at skipped wall-clock time");
+assert.compareArray(actual, expected, "order of operations with preceding midnight at skipped wall-clock time");
actual.splice(0); // clear
const expectedSkippedResult = [
@@ -131,14 +99,6 @@ const expectedSkippedResult = [
"get this.timeZone.getPossibleInstantsFor",
// GetPlainDateTimeFor on receiver's instant
"call this.timeZone.getOffsetNanosecondsFor",
- // GetInstantFor on preceding midnight
- "call this.timeZone.getPossibleInstantsFor",
- // AddDaysToZonedDateTime
- "call this.timeZone.getPossibleInstantsFor",
- // DisambiguatePossibleInstants
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getPossibleInstantsFor",
// InterpretISODateTimeOffset
"call this.timeZone.getPossibleInstantsFor",
// DisambiguatePossibleInstants
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/smallest-unit-day-rounding-modes.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/smallest-unit-day-rounding-modes.js
index f694712457..435bfa16d8 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/smallest-unit-day-rounding-modes.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/smallest-unit-day-rounding-modes.js
@@ -41,21 +41,22 @@ class TimeZone extends Temporal.TimeZone {
#count = 0;
#nanoseconds;
- constructor(nanoseconds) {
+ constructor(todayEpochNanoseconds, tomorrowEpochNanoseconds) {
super("UTC");
- this.#nanoseconds = nanoseconds;
+ this.#nanoseconds = [todayEpochNanoseconds, tomorrowEpochNanoseconds];
}
getPossibleInstantsFor(dateTime) {
- if (++this.#count === 2) {
- return [new Temporal.Instant(this.#nanoseconds)];
+ const nanoseconds = this.#nanoseconds[this.#count++];
+ if (nanoseconds === undefined) {
+ return super.getPossibleInstantsFor(dateTime);
}
- return super.getPossibleInstantsFor(dateTime);
+ return [new Temporal.Instant(nanoseconds)];
}
}
-function test(epochNanoseconds, tomorrowEpochNanoseconds, testCases) {
+function test(epochNanoseconds, todayEpochNanoseconds, tomorrowEpochNanoseconds, testCases) {
for (let [roundingMode, expected] of Object.entries(testCases)) {
- let timeZone = new TimeZone(tomorrowEpochNanoseconds);
+ let timeZone = new TimeZone(todayEpochNanoseconds, tomorrowEpochNanoseconds);
let zoned = new Temporal.ZonedDateTime(epochNanoseconds, timeZone);
let result = zoned.round({ smallestUnit: "days", roundingMode });
assert.sameValue(result.epochNanoseconds, expected);
@@ -64,31 +65,35 @@ function test(epochNanoseconds, tomorrowEpochNanoseconds, testCases) {
const oneDay = 24n * 60n * 60n * 1000n * 1000n * 1000n;
-// Test positive divisor (dayLengthNs).
-test(3n, 10n, {
- ceil: oneDay,
+test(3n, undefined, 10n, {
+ ceil: 10n, // end-of-day according to TimeZone protocol
floor: 0n,
trunc: 0n,
halfExpand: 0n,
});
-test(-3n, 10n, {
- ceil: 0n,
+test(-3n, undefined, 10n, {
+ ceil: 10n, // end-of-day according to TimeZone protocol
floor: -oneDay,
trunc: -oneDay,
- halfExpand: 0n,
+ halfExpand: 10n, // end-of-day according to TimeZone protocol
});
-test(-3n, -10n, {
- ceil: oneDay,
- floor: 0n,
- trunc: 0n,
- halfExpand: 0n,
-});
+assert.throws(RangeError, () => {
+ test(-3n, 0n, 10n, { ceil: undefined });
+}, "instant is before TimeZone protocol's start-of-day");
+
+assert.throws(RangeError, () => {
+ test(-3n, undefined, -10n, { ceil: undefined });
+}, "instant is after TimeZone protocol's end-of-day");
+
+assert.throws(RangeError, () => {
+ test(0n, 0n, 0n, { ceil: undefined });
+}, "instant is within zero-duration day");
// Test values at int64 boundaries.
-test(3n, /*INT64_MAX=*/ 9223372036854775807n, {
- ceil: oneDay,
+test(3n, undefined, /*INT64_MAX=*/ 9223372036854775807n, {
+ ceil: /*INT64_MAX=*/ 9223372036854775807n, // end-of-day according to TimeZone protocol
floor: 0n,
trunc: 0n,
halfExpand: 0n,
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/timezone-getpossibleinstantsfor-iterable.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/timezone-getpossibleinstantsfor-iterable.js
index 922b114e0b..28c064e24d 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/timezone-getpossibleinstantsfor-iterable.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/round/timezone-getpossibleinstantsfor-iterable.js
@@ -23,8 +23,6 @@ features: [Temporal]
---*/
const expected = [
- "2001-09-09T00:00:00", // called once on midnight of the input datetime
- "2001-09-10T00:00:00", // called once on the previous value plus one calendar day
"2001-09-09T02:00:00", // called once on the rounding result
];
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..6eeb874839
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,30 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const arg = { year: 1970, monthCode: "M01", day: 1, timeZone, calendar };
+ const result = instance.since(arg);
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, `Calendar created from string "${calendar}"`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-year-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-year-zero.js
index 52a3aee0f5..b5dc42f348 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-year-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-calendar-year-zero.js
@@ -17,7 +17,8 @@ const invalidStrings = [
];
const timeZone = new Temporal.TimeZone("UTC");
const instance = new Temporal.ZonedDateTime(0n, timeZone);
-invalidStrings.forEach((arg) => {
+invalidStrings.forEach((str) => {
+ const arg = { year: 1976, month: 11, day: 18, calendar: str };
assert.throws(
RangeError,
() => instance.since(arg),
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..c5181e85e0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-backward-offset-shift.js
@@ -0,0 +1,50 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.since(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..d6f7970e8a
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-propertybag-out-of-range-forward-offset-shift.js
@@ -0,0 +1,45 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.since(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..5d0053cad5
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["1970-01-01T00:00[UTC][U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01T00:00[UTC][u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01T00:00[UTC][FOO=bar]", "invalid capitalized unrecognized key"],
+];
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => instance.since(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/calendar-dateadd-called-with-options-undefined.js
index 7a5367bf29..932847c329 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/calendar-dateadd-called-with-options-undefined.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/calendar-dateadd-called-with-options-undefined.js
@@ -14,27 +14,17 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarDateAddUndefinedOptions();
const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9);
const earlier = new Temporal.ZonedDateTime(0n, timeZone, calendar);
-
-// Basic difference with largestUnit larger than days.
-// The call comes from this path:
-// ZonedDateTime.since() -> DifferenceZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
-
-const later1 = new Temporal.ZonedDateTime(1_213_200_000_000_000n, timeZone, calendar);
-later1.since(earlier, { largestUnit: "weeks" });
-assert.sameValue(calendar.dateAddCallCount, 1, "basic difference with largestUnit >days");
+const later = new Temporal.ZonedDateTime(1_213_200_000_000_000n, timeZone, calendar);
// Difference with rounding, with smallestUnit a calendar unit.
// The calls come from these paths:
// ZonedDateTime.since() ->
-// DifferenceZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
// RoundDuration ->
// MoveRelativeZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
// MoveRelativeDate -> calendar.dateAdd()
// BalanceDurationRelative -> MoveRelativeDate -> calendar.dateAdd()
-calendar.dateAddCallCount = 0;
-
-later1.since(earlier, { smallestUnit: "weeks" });
-assert.sameValue(calendar.dateAddCallCount, 4, "rounding difference with calendar smallestUnit");
+later.since(earlier, { smallestUnit: "weeks" });
+assert.sameValue(calendar.dateAddCallCount, 3, "rounding difference with calendar smallestUnit");
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/differencezoneddatetime-inconsistent-custom-calendar.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/differencezoneddatetime-inconsistent-custom-calendar.js
new file mode 100644
index 0000000000..391f978f0d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/differencezoneddatetime-inconsistent-custom-calendar.js
@@ -0,0 +1,56 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: >
+ Throws a RangeError when custom calendar method returns inconsistent result
+info: |
+ DifferenceZonedDateTime ( ... )
+ 8. Repeat 3 times:
+ ...
+ g. If _sign_ = 0, or _timeSign_ = 0, or _sign_ = _timeSign_, then
+ ...
+ viii. Return ? CreateNormalizedDurationRecord(_dateDifference_.[[Years]],
+ _dateDifference_.[[Months]], _dateDifference_.[[Weeks]],
+ _dateDifference_.[[Days]], _norm_).
+ h. Set _dayCorrection_ to _dayCorrection_ + 1.
+ 9. NOTE: This step is only reached when custom calendar or time zone methods
+ return inconsistent values.
+ 10. Throw a *RangeError* exception.
+features: [Temporal]
+---*/
+
+// Based partly on a test case by André Bargull
+
+const fiftyDays12Hours = 50n * 86400_000_000_000n + 12n * 3600_000_000_000n;
+
+{
+ const tz = new (class extends Temporal.TimeZone {
+ getPossibleInstantsFor(dateTime) {
+ return super.getPossibleInstantsFor(dateTime.subtract({ days: 3 }));
+ }
+ })("UTC");
+
+ const zdt1 = new Temporal.ZonedDateTime(0n, tz);
+ const zdt2 = new Temporal.ZonedDateTime(fiftyDays12Hours, tz);
+
+ assert.throws(RangeError, () => zdt2.since(zdt1, { largestUnit: "weeks" }),
+ "Calendar calculation where more than 2 days correction is needed should cause RangeError");
+}
+
+{
+ const cal = new (class extends Temporal.Calendar {
+ dateUntil(one, two, options) {
+ return super.dateUntil(one, two, options).negated();
+ }
+ })("iso8601");
+
+ const zdt1 = new Temporal.ZonedDateTime(0n, "UTC", cal);
+ const zdt2 = new Temporal.ZonedDateTime(fiftyDays12Hours, "UTC", cal);
+
+ assert.throws(RangeError, () => zdt2.since(zdt1, { largestUnit: "weeks" }),
+ "Calendar calculation causing mixed-sign values should cause RangeError");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-month-day-boundary.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-month-day-boundary.js
new file mode 100644
index 0000000000..c45ebd5aa2
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-month-day-boundary.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: >
+ Difference with the endpoint being the end of a skipped hour, chooses the
+ smaller of two possible durations
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Based on a test case by Adam Shaw
+
+const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
+
+const d1 = new Temporal.ZonedDateTime(957258000_000_000_000n /* = 2000-05-02T02:00-07:00 */, timeZone);
+const d2 = new Temporal.ZonedDateTime(954669600_000_000_000n /* = 2000-04-02T03:00-07:00 */, timeZone);
+// NOTE: nonexistent hour just before d2
+
+const result = d1.since(d2, { largestUnit: "months" });
+
+TemporalHelpers.assertDuration(
+ result, 0, 0, 0, 29, 23, 0, 0, 0, 0, 0,
+ "Result should not balance up to months, but pick the smaller of two possible durations"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js
new file mode 100644
index 0000000000..c3a0f3f3a2
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js
@@ -0,0 +1,48 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: >
+ Rounding the resulting duration takes the time zone's UTC offset shifts
+ into account
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Based on a test case by Adam Shaw
+
+const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
+
+{
+ // Month-only part of duration lands on skipped DST hour, should not cause
+ // disambiguation
+ const start = new Temporal.ZonedDateTime(
+ 950868000_000_000_000n /* = 2000-02-18T10Z */,
+ timeZone); /* = 2000-02-18T02-08 in local time */
+ const end = new Temporal.ZonedDateTime(
+ 954709200_000_000_000n /* = 2000-04-02T21Z */,
+ timeZone); /* = 2000-04-02T14-07 in local time */
+
+ const duration = start.since(end, { largestUnit: "months" });
+ TemporalHelpers.assertDuration(duration, 0, -1, 0, -15, -11, 0, 0, 0, 0, 0,
+ "1-month rounding window is shortened by DST");
+}
+
+
+{
+ // Month-only part of duration lands on skipped DST hour, should not cause
+ // disambiguation
+ const start = new Temporal.ZonedDateTime(
+ 951991200_000_000_000n /* = 2000-03-02T10Z */,
+ timeZone); /* = 2000-03-02T02-08 in local time */
+ const end = new Temporal.ZonedDateTime(
+ 956005200_000_000_000n /* = 2000-04-17T21Z */,
+ timeZone); /* = 2000-04-17T14-07 in local time */
+
+ const duration = start.since(end, { largestUnit: "months" });
+ TemporalHelpers.assertDuration(duration, 0, -1, 0, -15, -12, 0, 0, 0, 0, 0,
+ "1-month rounding window is not shortened by DST");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js
new file mode 100644
index 0000000000..7ef187ce3b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js
@@ -0,0 +1,84 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ Up to 3 intermediate instants may be tried when calculating ZonedDateTime
+ difference
+includes: [compareArray.js, temporalHelpers.js]
+features: [BigInt, Temporal]
+---*/
+
+const calls = [];
+
+const springFallZone = TemporalHelpers.springForwardFallBackTimeZone();
+TemporalHelpers.observeMethod(calls, springFallZone, "getPossibleInstantsFor");
+
+const dateLineZone = TemporalHelpers.crossDateLineTimeZone();
+TemporalHelpers.observeMethod(calls, dateLineZone, "getPossibleInstantsFor");
+
+const zdt2 = new Temporal.ZonedDateTime(946722600_000_000_000n /* = 2000-01-01T02:30 local */, springFallZone);
+
+// Future -> past, without wall-clock overshoot
+// Expects valid intermediate Instant WITHOUT day correction (computed once)
+{
+ const zdt1 = new Temporal.ZonedDateTime(949442400_000_000_000n /* = 2000-02-01T14:00 local */, springFallZone);
+ const result = zdt1.since(zdt2, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 1, 0, 0, 11, 30, 0, 0, 0, 0, "no wall-clock overshoot, no DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ ], "one intermediate should be tried");
+}
+
+calls.splice(0); // clear
+
+// Future -> past, WITH wall-clock overshoot
+// Expects valid intermediate Instant with guaranteed 1-DAY correction (computed once)
+{
+ const zdt1 = new Temporal.ZonedDateTime(949395600_000_000_000n /* = 2000-02-01T01:00 local */, springFallZone);
+ const result = zdt1.since(zdt2, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 30, 22, 30, 0, 0, 0, 0, "wall-clock overshoot, no DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ ], "one intermediate should be tried");
+}
+
+calls.splice(0); // clear
+
+// Future -> past, WITH wall-clock overshoot
+// Expects valid intermediate Instant with guaranteed 1-DAY correction (computed once)
+// Intermediate Instant falls within spring DST gap and gets pushed forward,
+// but since moving from future -> past, not possible to exacerbate overflow,
+// so no other day corrections.
+{
+ const end = new Temporal.ZonedDateTime(957258000_000_000_000n /* = 2000-05-02T02:00 local */, springFallZone);
+ const start = new Temporal.ZonedDateTime(954671400_000_000_000n /* = 2000-04-02T03:30-07:00 local */, springFallZone);
+ const result = end.since(start, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 29, 22, 30, 0, 0, 0, 0, "wall-clock overshoot, inconsiquential DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ ], "one intermediate should be tried");
+}
+
+calls.splice(0); // clear
+
+// Past -> future, WITH wall-clock overshoot
+// Tries intermediate Instant with 1-DAY correction (first compute)
+// Then, ANOTHER day correction because updated intermediate Instant falls within dateline DST gap,
+// pushing it forward, causing wall-clock overshoot again
+// (Not possible when going backwards)
+// (This test is just the same as the corresponding one in until(), but negative)
+{
+ const start = new Temporal.ZonedDateTime(1325102400_000_000_000n /* = 2011-12-28T10:00 local */, dateLineZone);
+ const end = new Temporal.ZonedDateTime(1325257200_000_000_000n /* = 2011-12-31T05:00 local */, dateLineZone);
+ const result = start.since(end, { largestUnit: "days" });
+ TemporalHelpers.assertDuration(result, 0, 0, 0, -1, -19, 0, 0, 0, 0, 0, "wall-clock overshoot, consiquential DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ "call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
+ "call getPossibleInstantsFor", // DisambiguatePossibleInstants on second intermediate
+ ], "two intermediates should be tried, with disambiguation");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-loop-arbitrarily.js
index 23f993f25f..454d5d216a 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-loop-arbitrarily.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-loop-arbitrarily.js
@@ -5,74 +5,42 @@
/*---
esid: sec-temporal.zoneddatetime.prototype.since
description: >
- NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer
+ NormalizedTimeDurationToDays should not be able to loop arbitrarily.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
...
- 21. Repeat, while done is false,
- a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]],
- relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign).
- b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]],
- relativeResult.[[EpochNanoseconds]]).
- c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs).
- c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then
- i. Set norm to oneDayLess.
- ii. Set relativeResult to oneDayFarther.
- iii. Set days to days + sign.
- d. Else,
- i. Set done to true.
-includes: [temporalHelpers.js]
+ 22. If NormalizedTimeDurationSign(_oneDayLess_) × _sign_ ≥ 0, then
+ a. Set _norm_ to _oneDayLess_.
+ b. Set _relativeResult_ to _oneDayFarther_.
+ c. Set _days_ to _days_ + _sign_.
+ d. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
+ e. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], relativeResult.[[EpochNanoseconds]]).
+ f. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) × _sign_ ≥ 0, then
+ i. Throw a *RangeError* exception.
features: [Temporal]
---*/
-const calls = [];
const dayLengthNs = 86400000000000n;
-const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
-
-function createRelativeTo(count) {
- const dayInstant = new Temporal.Instant(dayLengthNs);
- const substitutions = [];
- const timeZone = new Temporal.TimeZone("UTC");
- // Return constant value for first _count_ calls
- TemporalHelpers.substituteMethod(
- timeZone,
- "getPossibleInstantsFor",
- substitutions
- );
- substitutions.length = count;
- let i = 0;
- for (i = 0; i < substitutions.length; i++) {
- // (this value)
- substitutions[i] = [dayInstant];
+const dayInstant = new Temporal.Instant(dayLengthNs);
+let calls = 0;
+const timeZone = new class extends Temporal.TimeZone {
+ getPossibleInstantsFor() {
+ calls++;
+ return [dayInstant];
}
- // Record calls in calls[]
- TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor");
- return new Temporal.ZonedDateTime(0n, timeZone);
-}
-
-let zdt = createRelativeTo(50);
-calls.splice(0); // Reset calls list after ZonedDateTime construction
-zdt.since(other, {
- largestUnit: "day",
-});
-assert.sameValue(
- calls.length,
- 50 + 1,
- "Expected ZonedDateTime.since to call getPossibleInstantsFor correct number of times"
-);
+}("UTC");
-zdt = createRelativeTo(100);
-calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
-zdt.since(other, {
- largestUnit: "day",
-});
-assert.sameValue(
- calls.length,
- 100 + 1,
- "Expected ZonedDateTime.since to call getPossibleInstantsFor correct number of times"
-);
+const zdt = new Temporal.ZonedDateTime(0n, timeZone);
+const other = new Temporal.ZonedDateTime(dayLengthNs * 2n, "UTC", "iso8601");
-zdt = createRelativeTo(105);
-assert.throws(RangeError, () => zdt.since(other, { largestUnit: "day" }), "105 days > 2⁵³ ns");
+assert.throws(RangeError, () => zdt.since(other, { largestUnit: "day", smallestUnit: "second" }), "indefinite loop is prevented");
+assert.sameValue(calls, 4, "getPossibleInstantsFor is not called indefinitely");
+ // Expected calls:
+ // DifferenceTemporalZonedDateTime ->
+ // DifferenceZonedDateTime -> GetInstantFor (1)
+ // NormalizedTimeDurationToDays ->
+ // AddDaysToZonedDateTime (2, step 12)
+ // AddDaysToZonedDateTime (3, step 15)
+ // AddDaysToZonedDateTime (4, step 18.d)
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-range-errors.js
index 0c0b07f5dc..a6af0a69bc 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-range-errors.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/normalized-time-duration-to-days-range-errors.js
@@ -8,12 +8,12 @@ description: >
RangeErrors.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
- 22. If days < 0 and sign = 1, throw a RangeError exception.
- 23. If days > 0 and sign = -1, throw a RangeError exception.
+ 23. If days < 0 and sign = 1, throw a RangeError exception.
+ 24. If days > 0 and sign = -1, throw a RangeError exception.
...
- 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
+ 26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
...
- 28. If dayLength ≥ 2⁵³, throw a RangeError exception.
+ 29. If dayLength ≥ 2⁵³, throw a RangeError exception.
features: [Temporal, BigInt]
includes: [temporalHelpers.js]
---*/
@@ -40,13 +40,16 @@ const dayNs = 86_400_000_000_000;
const zeroZDT = new Temporal.ZonedDateTime(0n, "UTC");
const oneZDT = new Temporal.ZonedDateTime(1n, "UTC");
const epochInstant = new Temporal.Instant(0n);
-const options = { largestUnit: "days" };
+const options = { largestUnit: "days", smallestUnit: "seconds", roundingMode: "expand" };
-// Step 22: days < 0 and sign = 1
+// Step 23: days < 0 and sign = 1
let start = new Temporal.ZonedDateTime(
0n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [epochInstant], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -60,14 +63,18 @@ assert.throws(RangeError, () =>
start.since(
oneZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "days < 0 and sign = 1"
);
-// Step 23: days > 0 and sign = -1
+// Step 24: days > 0 and sign = -1
start = new Temporal.ZonedDateTime(
1n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [epochInstant], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -81,14 +88,18 @@ assert.throws(RangeError, () =>
start.since(
zeroZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "days > 0 and sign = -1"
);
-// Step 25: nanoseconds > 0 and sign = -1
+// Step 26: nanoseconds > 0 and sign = -1
start = new Temporal.ZonedDateTime(
1n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[new Temporal.Instant(-1n)]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [new Temporal.Instant(-2_000_000_000n)], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -102,16 +113,20 @@ assert.throws(RangeError, () =>
start.since(
zeroZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "norm > 0 and sign = -1"
);
-// Step 28: day length is an unsafe integer
+// Step 29: day length is an unsafe integer
start = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
- // Not called in step 16 because _days_ = 0
- // Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_
- [[new Temporal.Instant(2n ** 53n)]],
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ // Not called in step 16 because _days_ = 0
+ // Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_
+ [new Temporal.Instant(2n ** 53n)],
+ ],
[]
)
);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/order-of-operations.js
index 566a1ce192..90fc1e0475 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/order-of-operations.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/order-of-operations.js
@@ -306,10 +306,6 @@ assert.compareArray(actual, [
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays → AddDaysToZonedDateTime
"call this.timeZone.getPossibleInstantsFor",
], "order of operations with identical wall-clock times and largestUnit a calendar unit");
actual.splice(0); // clear
@@ -328,17 +324,8 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
- // DifferenceISODateTime
- "call this.calendar.dateUntil",
- // AddZonedDateTime
- "call this.calendar.dateAdd",
- "call this.timeZone.getPossibleInstantsFor",
- // NanosecondsToDays
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays → AddDaysToZonedDateTime
- "call this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
+ "call this.calendar.dateUntil",
];
const expectedOpsForCalendarRounding = [
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/wrapping-at-end-of-month.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/wrapping-at-end-of-month.js
new file mode 100644
index 0000000000..3fddf0d09b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/since/wrapping-at-end-of-month.js
@@ -0,0 +1,120 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.since
+description: Tests balancing of days to months at end of month
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Difference between end of longer month to end of following shorter month
+{
+ const end = new Temporal.ZonedDateTime(5011200_000_000_000n /* = 1970-02-28T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2332800_000_000_000n /* = 1970-01-28T00Z */, "UTC").since(end, { largestUnit }),
+ 0, -1, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Jan 28th to Feb 28th is one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2419200_000_000_000n /* = 1970-01-29T00Z */, "UTC").since(end, { largestUnit }),
+ 0, 0, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Feb 28th is 30 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2505600_000_000_000n /* = 1970-01-30T00Z */, "UTC").since(end, { largestUnit }),
+ 0, 0, 0, -29, 0, 0, 0, 0, 0, 0,
+ "Jan 30th to Feb 28th is 29 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2592000_000_000_000n /* = 1970-01-31T00Z */, "UTC").since(end, { largestUnit }),
+ 0, 0, 0, -28, 0, 0, 0, 0, 0, 0,
+ "Jan 31st to Feb 28th is 28 days, not one month"
+ );
+ }
+}
+
+// Difference between end of leap-year January to end of leap-year February
+{
+ const end = new Temporal.ZonedDateTime(68169600_000_000_000n /* = 1972-02-29T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65491200_000_000_000n /* = 1972-01-29T00Z */, "UTC").since(end, { largestUnit }),
+ 0, -1, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Feb 29th is one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65577600_000_000_000n /* = 1972-01-30T00Z */, "UTC").since(end, { largestUnit }),
+ 0, 0, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Jan 30th to Feb 29th is 30 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65664000_000_000_000n /* = 1972-01-31T00Z */, "UTC").since(end, { largestUnit }),
+ 0, 0, 0, -29, 0, 0, 0, 0, 0, 0,
+ "Jan 31st to Feb 29th is 29 days, not one month"
+ );
+ }
+}
+
+// Difference between end of longer month to end of not-immediately-following
+// shorter month
+{
+ const end = new Temporal.ZonedDateTime(28771200_000_000_000n /* = 1970-11-30T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(20822400_000_000_000n /* = 1970-08-30T00Z */, "UTC").since(end, { largestUnit }),
+ 0, -3, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Aug 30th to Nov 30th is 3 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(20908800_000_000_000n /* = 1970-08-31T00Z */, "UTC").since(end, { largestUnit }),
+ 0, -2, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Aug 31st to Nov 30th is 2 months 30 days, not 3 months"
+ );
+ }
+}
+
+// Difference between end of longer month in one year to shorter month in
+// later year
+{
+ const end = new Temporal.ZonedDateTime(104976000_000_000_000n /* = 1973-04-30T00Z */, "UTC");
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31363200_000_000_000n /* = 1970-12-30T00Z */, "UTC").since(end, { largestUnit: "months" }),
+ 0, -28, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 28 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31363200_000_000_000n /* = 1970-12-30T00Z */, "UTC").since(end, { largestUnit: "years" }),
+ -2, -4, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 2 years, 4 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31449600_000_000_000n /* = 1970-12-31T00Z */, "UTC").since(end, { largestUnit: "months" }),
+ 0, -27, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 27 months, 30 days, not 28 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31449600_000_000_000n /* = 1970-12-31T00Z */, "UTC").since(end, { largestUnit: "years" }),
+ -2, -3, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 2 years, 3 months, 30 days, not 2 years 4 months"
+ );
+}
+
+// Difference where months passes through a month that's the same length or
+// shorter than either the start or end month
+{
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2419200_000_000_000n /* = 1970-01-29T00Z */, "UTC").since(new Temporal.ZonedDateTime(7430400_000_000_000n /* = 1970-03-28T00Z */, "UTC"), { largestUnit: "months" }),
+ 0, -1, 0, -28, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Mar 28th is 1 month 28 days, not 58 days"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2592000_000_000_000n /* = 1970-01-31T00Z */, "UTC").since(new Temporal.ZonedDateTime(44409600_000_000_000n /* = 1971-05-30T00Z */, "UTC"), { largestUnit: "years" }),
+ -1, -3, 0, -30, 0, 0, 0, 0, 0, 0,
+ "Jan 31st 1970 to May 30th 1971 is 1 year, 3 months, 30 days, not 1 year, 2 months, 60 days"
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..d71f6be08e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.startofday
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.startOfDay();
+
+assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..82e86c4ee5
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
@@ -0,0 +1,44 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.startofday
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.startOfDay(), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..f1342dfe2d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,53 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.startofday
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.startOfDay();
+
+assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..06055c287e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/startOfDay/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.startofday
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.startOfDay(), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..b01b28a4bd
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,30 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const arg = { year: 1970, monthCode: "M01", day: 1, timeZone, calendar };
+ const result = instance.until(arg);
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, `Calendar created from string "${calendar}"`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-year-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-year-zero.js
index f0c915198e..f69f22d2b8 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-year-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-calendar-year-zero.js
@@ -17,7 +17,8 @@ const invalidStrings = [
];
const timeZone = new Temporal.TimeZone("UTC");
const instance = new Temporal.ZonedDateTime(0n, timeZone);
-invalidStrings.forEach((arg) => {
+invalidStrings.forEach((str) => {
+ const arg = { year: 1976, month: 11, day: 18, calendar: str };
assert.throws(
RangeError,
() => instance.until(arg),
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..bf1b4089f0
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-backward-offset-shift.js
@@ -0,0 +1,50 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.until(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..891307c99d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-propertybag-out-of-range-forward-offset-shift.js
@@ -0,0 +1,45 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+const arg = { year: 1970, month: 1, day: 1, hour: 12, timeZone };
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.until(arg), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..7226f0cb8c
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["1970-01-01T00:00[UTC][U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01T00:00[UTC][u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01T00:00[UTC][FOO=bar]", "invalid capitalized unrecognized key"],
+];
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => instance.until(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/calendar-dateadd-called-with-options-undefined.js
index 5710b9c2b4..14a26aa8ef 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/calendar-dateadd-called-with-options-undefined.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/calendar-dateadd-called-with-options-undefined.js
@@ -14,27 +14,17 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarDateAddUndefinedOptions();
const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9);
const earlier = new Temporal.ZonedDateTime(0n, timeZone, calendar);
-
-// Basic difference with largestUnit larger than days.
-// The call comes from this path:
-// ZonedDateTime.until() -> DifferenceZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
-
-const later1 = new Temporal.ZonedDateTime(1_213_200_000_000_000n, timeZone, calendar);
-earlier.until(later1, { largestUnit: "weeks" });
-assert.sameValue(calendar.dateAddCallCount, 1, "basic difference with largestUnit >days");
+const later = new Temporal.ZonedDateTime(1_213_200_000_000_000n, timeZone, calendar);
// Difference with rounding, with smallestUnit a calendar unit.
// The calls come from these paths:
// ZonedDateTime.until() ->
-// DifferenceZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
// RoundDuration ->
// MoveRelativeZonedDateTime -> AddZonedDateTime -> calendar.dateAdd()
// MoveRelativeDate -> calendar.dateAdd()
// BalanceDurationRelative -> MoveRelativeDate -> calendar.dateAdd()
-calendar.dateAddCallCount = 0;
-
-earlier.until(later1, { smallestUnit: "weeks" });
-assert.sameValue(calendar.dateAddCallCount, 4, "rounding difference with calendar smallestUnit");
+earlier.until(later, { smallestUnit: "weeks" });
+assert.sameValue(calendar.dateAddCallCount, 3, "rounding difference with calendar smallestUnit");
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/differencezoneddatetime-inconsistent-custom-calendar.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/differencezoneddatetime-inconsistent-custom-calendar.js
new file mode 100644
index 0000000000..7b77608492
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/differencezoneddatetime-inconsistent-custom-calendar.js
@@ -0,0 +1,56 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ Throws a RangeError when custom calendar method returns inconsistent result
+info: |
+ DifferenceZonedDateTime ( ... )
+ 8. Repeat 3 times:
+ ...
+ g. If _sign_ = 0, or _timeSign_ = 0, or _sign_ = _timeSign_, then
+ ...
+ viii. Return ? CreateNormalizedDurationRecord(_dateDifference_.[[Years]],
+ _dateDifference_.[[Months]], _dateDifference_.[[Weeks]],
+ _dateDifference_.[[Days]], _norm_).
+ h. Set _dayCorrection_ to _dayCorrection_ + 1.
+ 9. NOTE: This step is only reached when custom calendar or time zone methods
+ return inconsistent values.
+ 10. Throw a *RangeError* exception.
+features: [Temporal]
+---*/
+
+// Based partly on a test case by André Bargull
+
+const fiftyDays12Hours = 50n * 86400_000_000_000n + 12n * 3600_000_000_000n;
+
+{
+ const tz = new (class extends Temporal.TimeZone {
+ getPossibleInstantsFor(dateTime) {
+ return super.getPossibleInstantsFor(dateTime.add({ days: 3 }));
+ }
+ })("UTC");
+
+ const zdt1 = new Temporal.ZonedDateTime(0n, tz);
+ const zdt2 = new Temporal.ZonedDateTime(fiftyDays12Hours, tz);
+
+ assert.throws(RangeError, () => zdt1.until(zdt2, { largestUnit: "weeks" }),
+ "Calendar calculation where more than 2 days correction is needed should cause RangeError");
+}
+
+{
+ const cal = new (class extends Temporal.Calendar {
+ dateUntil(one, two, options) {
+ return super.dateUntil(one, two, options).negated();
+ }
+ })("iso8601");
+
+ const zdt1 = new Temporal.ZonedDateTime(0n, "UTC", cal);
+ const zdt2 = new Temporal.ZonedDateTime(fiftyDays12Hours, "UTC", cal);
+
+ assert.throws(RangeError, () => zdt1.until(zdt2, { largestUnit: "weeks" }),
+ "Calendar calculation causing mixed-sign values should cause RangeError");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-month-day-boundary.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-month-day-boundary.js
new file mode 100644
index 0000000000..73596e06c5
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-month-day-boundary.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ Difference with the endpoint being the end of a skipped hour, chooses the
+ smaller of two possible durations
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Based on a test case by Adam Shaw
+
+const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
+
+const d1 = new Temporal.ZonedDateTime(957258000_000_000_000n /* = 2000-05-02T02:00-07:00 */, timeZone);
+const d2 = new Temporal.ZonedDateTime(954669600_000_000_000n /* = 2000-04-02T03:00-07:00 */, timeZone);
+// NOTE: nonexistent hour just before d2
+
+const result = d1.until(d2, { largestUnit: "months" });
+
+TemporalHelpers.assertDuration(
+ result, 0, 0, 0, -29, -23, 0, 0, 0, 0, 0,
+ "Result should not balance up to months, but pick the smaller of two possible durations"
+);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js
new file mode 100644
index 0000000000..e6faed63f3
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js
@@ -0,0 +1,48 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ Rounding the resulting duration takes the time zone's UTC offset shifts
+ into account
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Based on a test case by Adam Shaw
+
+const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
+
+{
+ // Month-only part of duration lands on skipped DST hour, should not cause
+ // disambiguation
+ const start = new Temporal.ZonedDateTime(
+ 950868000_000_000_000n /* = 2000-02-18T10Z */,
+ timeZone); /* = 2000-02-18T02-08 in local time */
+ const end = new Temporal.ZonedDateTime(
+ 954709200_000_000_000n /* = 2000-04-02T21Z */,
+ timeZone); /* = 2000-04-02T14-07 in local time */
+
+ const duration = start.until(end, { largestUnit: "months" });
+ TemporalHelpers.assertDuration(duration, 0, 1, 0, 15, 11, 0, 0, 0, 0, 0,
+ "1-month rounding window is shortened by DST");
+}
+
+
+{
+ // Month-only part of duration lands on skipped DST hour, should not cause
+ // disambiguation
+ const start = new Temporal.ZonedDateTime(
+ 951991200_000_000_000n /* = 2000-03-02T10Z */,
+ timeZone); /* = 2000-03-02T02-08 in local time */
+ const end = new Temporal.ZonedDateTime(
+ 956005200_000_000_000n /* = 2000-04-17T21Z */,
+ timeZone); /* = 2000-04-17T14-07 in local time */
+
+ const duration = start.until(end, { largestUnit: "months" });
+ TemporalHelpers.assertDuration(duration, 0, 1, 0, 15, 12, 0, 0, 0, 0, 0,
+ "1-month rounding window is not shortened by DST");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js
new file mode 100644
index 0000000000..9440f950a6
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js
@@ -0,0 +1,85 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: >
+ Up to 3 intermediate instants may be tried when calculating ZonedDateTime
+ difference
+includes: [compareArray.js, temporalHelpers.js]
+features: [BigInt, Temporal]
+---*/
+
+const calls = [];
+
+const springFallZone = TemporalHelpers.springForwardFallBackTimeZone();
+TemporalHelpers.observeMethod(calls, springFallZone, "getPossibleInstantsFor");
+
+const dateLineZone = TemporalHelpers.crossDateLineTimeZone();
+TemporalHelpers.observeMethod(calls, dateLineZone, "getPossibleInstantsFor");
+
+const zdt1 = new Temporal.ZonedDateTime(946722600_000_000_000n /* = 2000-01-01T02:30 local */, springFallZone);
+
+// Past -> future, without wall-clock overshoot
+// Expects valid intermediate Instant WITHOUT day correction (computed once)
+{
+ const zdt2 = new Temporal.ZonedDateTime(949442400_000_000_000n /* = 2000-02-01T14:00 local */, springFallZone);
+ const result = zdt1.until(zdt2, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 1, 0, 0, 11, 30, 0, 0, 0, 0, "no wall-clock overshoot, no DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ ], "one intermediate should be tried");
+}
+
+calls.splice(0); // clear
+
+// Past -> future, WITH wall-clock overshoot
+// Expects valid intermediate Instant with guaranteed 1-DAY correction (computed once)
+{
+ const zdt2 = new Temporal.ZonedDateTime(949395600_000_000_000n /* = 2000-02-01T01:00 local */, springFallZone);
+ const result = zdt1.until(zdt2, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 30, 22, 30, 0, 0, 0, 0, "wall-clock overshoot, no DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ ], "one intermediate should be tried");
+}
+
+calls.splice(0); // clear
+
+// Past -> future, WITH wall-clock overshoot
+// Tries intermediate Instant with 1-DAY correction (first compute)
+// Then, ANOTHER correction because updated intermediate Instant falls within spring DST gap,
+// pushing it forward, causing wall-clock overshoot again
+// (Not possible when going backwards)
+{
+ const zdt2 = new Temporal.ZonedDateTime(954669600_000_000_000n /* = 2000-04-02T02:00 local */, springFallZone);
+ const result = zdt1.until(zdt2, { largestUnit: "years" });
+ TemporalHelpers.assertDuration(result, 0, 3, 0, 0, 23, 30, 0, 0, 0, 0, "wall-clock overshoot, small consiquential DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ "call getPossibleInstantsFor", // DisambiguatePossibleInstants on first intermediate
+ "call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
+ ], "two intermediates should be tried, with disambiguation");
+}
+
+calls.splice(0); // clear
+
+// Past -> future, WITH wall-clock overshoot
+// Tries intermediate Instant with 1-DAY correction (first compute)
+// Then, ANOTHER correction because updated intermediate Instant falls within dateline DST gap,
+// pushing it forward, causing wall-clock overshoot again
+// (Not possible when going backwards)
+// (This test is just the same as the corresponding one in since(), but negative)
+{
+ const start = new Temporal.ZonedDateTime(1325102400_000_000_000n /* = 2011-12-28T10:00 local */, dateLineZone);
+ const end = new Temporal.ZonedDateTime(1325257200_000_000_000n /* = 2011-12-31T05:00 local */, dateLineZone);
+ const result = start.until(end, { largestUnit: "days" });
+ TemporalHelpers.assertDuration(result, 0, 0, 0, 1, 19, 0, 0, 0, 0, 0, "wall-clock overshoot, big consiquential DST");
+ assert.compareArray(calls, [
+ "call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
+ "call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
+ "call getPossibleInstantsFor", // DisambiguatePossibleInstants on second intermediate
+ ], "two intermediates should be tried, with disambiguation");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-loop-arbitrarily.js
index f057d733d9..77886a2042 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-loop-arbitrarily.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-loop-arbitrarily.js
@@ -5,74 +5,42 @@
/*---
esid: sec-temporal.zoneddatetime.prototype.until
description: >
- NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer
+ NormalizedTimeDurationToDays should not be able to loop arbitrarily.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
...
- 21. Repeat, while done is false,
- a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]],
- relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign).
- b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]],
- relativeResult.[[EpochNanoseconds]]).
- c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs).
- c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then
- i. Set norm to oneDayLess.
- ii. Set relativeResult to oneDayFarther.
- iii. Set days to days + sign.
- d. Else,
- i. Set done to true.
-includes: [temporalHelpers.js]
+ 22. If NormalizedTimeDurationSign(_oneDayLess_) × _sign_ ≥ 0, then
+ a. Set _norm_ to _oneDayLess_.
+ b. Set _relativeResult_ to _oneDayFarther_.
+ c. Set _days_ to _days_ + _sign_.
+ d. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_).
+ e. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], relativeResult.[[EpochNanoseconds]]).
+ f. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) × _sign_ ≥ 0, then
+ i. Throw a *RangeError* exception.
features: [Temporal]
---*/
-const calls = [];
const dayLengthNs = 86400000000000n;
-const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
-
-function createRelativeTo(count) {
- const dayInstant = new Temporal.Instant(dayLengthNs);
- const substitutions = [];
- const timeZone = new Temporal.TimeZone("UTC");
- // Return constant value for first _count_ calls
- TemporalHelpers.substituteMethod(
- timeZone,
- "getPossibleInstantsFor",
- substitutions
- );
- substitutions.length = count;
- let i = 0;
- for (i = 0; i < substitutions.length; i++) {
- // (this value)
- substitutions[i] = [dayInstant];
+const dayInstant = new Temporal.Instant(dayLengthNs);
+let calls = 0;
+const timeZone = new class extends Temporal.TimeZone {
+ getPossibleInstantsFor() {
+ calls++;
+ return [dayInstant];
}
- // Record calls in calls[]
- TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor");
- return new Temporal.ZonedDateTime(0n, timeZone);
-}
-
-let zdt = createRelativeTo(50);
-calls.splice(0); // Reset calls list after ZonedDateTime construction
-zdt.until(other, {
- largestUnit: "day",
-});
-assert.sameValue(
- calls.length,
- 50 + 1,
- "Expected ZonedDateTime.until to call getPossibleInstantsFor correct number of times"
-);
+}("UTC");
-zdt = createRelativeTo(100);
-calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
-zdt.until(other, {
- largestUnit: "day",
-});
-assert.sameValue(
- calls.length,
- 100 + 1,
- "Expected ZonedDateTime.until to call getPossibleInstantsFor correct number of times"
-);
+const zdt = new Temporal.ZonedDateTime(0n, timeZone);
+const other = new Temporal.ZonedDateTime(dayLengthNs * 2n, "UTC", "iso8601");
-zdt = createRelativeTo(105);
-assert.throws(RangeError, () => zdt.until(other, { largestUnit: "day" }), "105 days > 2⁵³ ns");
+assert.throws(RangeError, () => zdt.until(other, { largestUnit: "day", smallestUnit: "second" }), "indefinite loop is prevented");
+assert.sameValue(calls, 4, "getPossibleInstantsFor is not called indefinitely");
+ // Expected calls:
+ // DifferenceTemporalZonedDateTime ->
+ // DifferenceZonedDateTime -> GetInstantFor (1)
+ // NormalizedTimeDurationToDays ->
+ // AddDaysToZonedDateTime (2, step 12)
+ // AddDaysToZonedDateTime (3, step 15)
+ // AddDaysToZonedDateTime (4, step 18.d)
reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-range-errors.js
index 178b1c981b..479dcf0b0c 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-range-errors.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/normalized-time-duration-to-days-range-errors.js
@@ -8,12 +8,12 @@ description: >
RangeErrors.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
- 22. If days < 0 and sign = 1, throw a RangeError exception.
- 23. If days > 0 and sign = -1, throw a RangeError exception.
+ 23. If days < 0 and sign = 1, throw a RangeError exception.
+ 24. If days > 0 and sign = -1, throw a RangeError exception.
...
- 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
+ 26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
...
- 28. If dayLength ≥ 2⁵³, throw a RangeError exception.
+ 29. If dayLength ≥ 2⁵³, throw a RangeError exception.
features: [Temporal, BigInt]
includes: [temporalHelpers.js]
---*/
@@ -40,13 +40,16 @@ const dayNs = 86_400_000_000_000;
const zeroZDT = new Temporal.ZonedDateTime(0n, "UTC");
const oneZDT = new Temporal.ZonedDateTime(1n, "UTC");
const epochInstant = new Temporal.Instant(0n);
-const options = { largestUnit: "days" };
+const options = { largestUnit: "days", smallestUnit: "seconds", roundingMode: "expand" };
-// Step 22: days < 0 and sign = 1
+// Step 23: days < 0 and sign = 1
let start = new Temporal.ZonedDateTime(
0n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [epochInstant], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -60,14 +63,18 @@ assert.throws(RangeError, () =>
start.until(
oneZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "days < 0 and sign = 1"
);
-// Step 23: days > 0 and sign = -1
+// Step 24: days > 0 and sign = -1
start = new Temporal.ZonedDateTime(
1n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[epochInstant]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [epochInstant], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -81,14 +88,18 @@ assert.throws(RangeError, () =>
start.until(
zeroZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "days > 0 and sign = -1"
);
-// Step 25: nanoseconds > 0 and sign = -1
+// Step 26: nanoseconds > 0 and sign = -1
start = new Temporal.ZonedDateTime(
1n, // Sets DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
- [[new Temporal.Instant(-1n)]], // Returned in step 16, setting _relativeResult_
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ [new Temporal.Instant(-2_000_000_000n)], // Returned in step 16, setting _relativeResult_
+ ],
[
// Behave normally in 2 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
@@ -102,16 +113,20 @@ assert.throws(RangeError, () =>
start.until(
zeroZDT, // Sets DifferenceZonedDateTime _ns2_
options
- )
+ ),
+ "norm > 0 and sign = -1"
);
-// Step 28: day length is an unsafe integer
+// Step 29: day length is an unsafe integer
start = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
- // Not called in step 16 because _days_ = 0
- // Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_
- [[new Temporal.Instant(2n ** 53n)]],
+ [
+ TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally in DifferenceZonedDateTime
+ // Not called in step 16 because _days_ = 0
+ // Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_
+ [new Temporal.Instant(2n ** 53n)],
+ ],
[]
)
);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/order-of-operations.js
index 6a233dc989..c88ecfd0d6 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/order-of-operations.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/order-of-operations.js
@@ -306,10 +306,6 @@ assert.compareArray(actual, [
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays → AddDaysToZonedDateTime
"call this.timeZone.getPossibleInstantsFor",
], "order of operations with identical wall-clock times and largestUnit a calendar unit");
actual.splice(0); // clear
@@ -328,17 +324,8 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
- // DifferenceISODateTime
- "call this.calendar.dateUntil",
- // AddZonedDateTime
- "call this.calendar.dateAdd",
- "call this.timeZone.getPossibleInstantsFor",
- // NanosecondsToDays
- "call this.timeZone.getOffsetNanosecondsFor",
- "call this.timeZone.getOffsetNanosecondsFor",
- // NanosecondsToDays → AddDaysToZonedDateTime
- "call this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
+ "call this.calendar.dateUntil",
];
const expectedOpsForCalendarRounding = [
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/wrapping-at-end-of-month.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/wrapping-at-end-of-month.js
new file mode 100644
index 0000000000..50b40393f1
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/until/wrapping-at-end-of-month.js
@@ -0,0 +1,120 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.until
+description: Tests balancing of days to months at end of month
+includes: [temporalHelpers.js]
+features: [Temporal]
+---*/
+
+// Difference between end of longer month to end of following shorter month
+{
+ const end = new Temporal.ZonedDateTime(5011200_000_000_000n /* = 1970-02-28T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2332800_000_000_000n /* = 1970-01-28T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Jan 28th to Feb 28th is one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2419200_000_000_000n /* = 1970-01-29T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 0, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Feb 28th is 30 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2505600_000_000_000n /* = 1970-01-30T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 0, 0, 29, 0, 0, 0, 0, 0, 0,
+ "Jan 30th to Feb 28th is 29 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2592000_000_000_000n /* = 1970-01-31T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 0, 0, 28, 0, 0, 0, 0, 0, 0,
+ "Jan 31st to Feb 28th is 28 days, not one month"
+ );
+ }
+}
+
+// Difference between end of leap-year January to end of leap-year February
+{
+ const end = new Temporal.ZonedDateTime(68169600_000_000_000n /* = 1972-02-29T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65491200_000_000_000n /* = 1972-01-29T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Feb 29th is one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65577600_000_000_000n /* = 1972-01-30T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 0, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Jan 30th to Feb 29th is 30 days, not one month"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(65664000_000_000_000n /* = 1972-01-31T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 0, 0, 29, 0, 0, 0, 0, 0, 0,
+ "Jan 31st to Feb 29th is 29 days, not one month"
+ );
+ }
+}
+
+// Difference between end of longer month to end of not-immediately-following
+// shorter month
+{
+ const end = new Temporal.ZonedDateTime(28771200_000_000_000n /* = 1970-11-30T00Z */, "UTC");
+ for (const largestUnit of ["years", "months"]) {
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(20822400_000_000_000n /* = 1970-08-30T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 3, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Aug 30th to Nov 30th is 3 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(20908800_000_000_000n /* = 1970-08-31T00Z */, "UTC").until(end, { largestUnit }),
+ 0, 2, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Aug 31st to Nov 30th is 2 months 30 days, not 3 months"
+ );
+ }
+}
+
+// Difference between end of longer month in one year to shorter month in
+// later year
+{
+ const end = new Temporal.ZonedDateTime(104976000_000_000_000n /* = 1973-04-30T00Z */, "UTC");
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31363200_000_000_000n /* = 1970-12-30T00Z */, "UTC").until(end, { largestUnit: "months" }),
+ 0, 28, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 28 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31363200_000_000_000n /* = 1970-12-30T00Z */, "UTC").until(end, { largestUnit: "years" }),
+ 2, 4, 0, 0, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 2 years, 4 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31449600_000_000_000n /* = 1970-12-31T00Z */, "UTC").until(end, { largestUnit: "months" }),
+ 0, 27, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 27 months, 30 days, not 28 months"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(31449600_000_000_000n /* = 1970-12-31T00Z */, "UTC").until(end, { largestUnit: "years" }),
+ 2, 3, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Dec 30th 1970 to Apr 30th 1973 is 2 years, 3 months, 30 days, not 2 years 4 months"
+ );
+}
+
+// Difference where months passes through a month that's the same length or
+// shorter than either the start or end month
+{
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2419200_000_000_000n /* = 1970-01-29T00Z */, "UTC").until(new Temporal.ZonedDateTime(7430400_000_000_000n /* = 1970-03-28T00Z */, "UTC"), { largestUnit: "months" }),
+ 0, 1, 0, 28, 0, 0, 0, 0, 0, 0,
+ "Jan 29th to Mar 28th is 1 month 28 days, not 58 days"
+ );
+ TemporalHelpers.assertDuration(
+ new Temporal.ZonedDateTime(2592000_000_000_000n /* = 1970-01-31T00Z */, "UTC").until(new Temporal.ZonedDateTime(44409600_000_000_000n /* = 1971-05-30T00Z */, "UTC"), { largestUnit: "years" }),
+ 1, 3, 0, 30, 0, 0, 0, 0, 0, 0,
+ "Jan 31st 1970 to May 30th 1971 is 1 year, 3 months, 30 days, not 1 year, 2 months, 60 days"
+ );
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/custom-calendar-weekofyear.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/custom-calendar-weekofyear.js
new file mode 100644
index 0000000000..43a7d2ba14
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/custom-calendar-weekofyear.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.weekofyear
+description: >
+ Temporal.ZonedDateTime.prototype.weekOfYear returns undefined for all
+ custom calendars where weekOfYear() returns undefined.
+features: [Temporal]
+---*/
+
+class CustomCalendar extends Temporal.Calendar {
+ constructor() {
+ super("iso8601");
+ }
+ weekOfYear() {
+ return undefined;
+ }
+}
+
+const calendar = new CustomCalendar();
+// Epoch Nanoseconds for new Temporal.PlainDateTime(2024, 1, 1, 12, 34, 56, 987, 654, 321, calendar);
+const customCalendarDate = new Temporal.ZonedDateTime(1_704_112_496_987_654_321n, "UTC", calendar);
+assert.sameValue(customCalendarDate.weekOfYear, undefined);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/validate-calendar-value.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/validate-calendar-value.js
index 8b18332ce4..b2b83c1d41 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/validate-calendar-value.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/weekOfYear/validate-calendar-value.js
@@ -9,7 +9,6 @@ features: [Temporal]
---*/
const badResults = [
- [undefined, TypeError],
[null, TypeError],
[false, TypeError],
[Infinity, RangeError],
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..a1c2cec71d
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,52 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.with
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n;
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+
+for (const disambiguation of ["earlier", "later", "compatible"]) {
+ instance.with({ day: 1 }, { disambiguation });
+
+ assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+ calls = 0;
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..a93ee7ff32
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
@@ -0,0 +1,46 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.with
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n;
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+for (const disambiguation of ["earlier", "later", "compatible"]) {
+ assert.throws(RangeError, () => instance.with({ day: 1 }, { disambiguation }), "RangeError should be thrown");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..b053782926
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,56 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.with
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+for (const disambiguation of ["earlier", "later", "compatible"]) {
+ instance.with({ day: 1 }, { disambiguation });
+
+ assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+ calls = 0;
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..7db6925321
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/with/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,51 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.with
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+for (const disambiguation of ["earlier", "later", "compatible"]) {
+ assert.throws(RangeError, () => instance.with({ day: 1 }, { disambiguation }), "RangeError should be thrown");
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withCalendar/calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withCalendar/calendar-iso-string.js
new file mode 100644
index 0000000000..834be0ebb6
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withCalendar/calendar-iso-string.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withcalendar
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+features: [Temporal]
+---*/
+
+const instance = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", {
+ dateAdd() {},
+ dateFromFields() {},
+ dateUntil() {},
+ day() {},
+ dayOfWeek() {},
+ dayOfYear() {},
+ daysInMonth() {},
+ daysInWeek() {},
+ daysInYear() {},
+ fields() {},
+ id: "replace-me",
+ inLeapYear() {},
+ mergeFields() {},
+ month() {},
+ monthCode() {},
+ monthDayFromFields() {},
+ monthsInYear() {},
+ weekOfYear() {},
+ year() {},
+ yearMonthFromFields() {},
+ yearOfWeek() {},
+});
+
+for (const arg of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const result = instance.withCalendar(arg);
+ assert.sameValue(result.getISOFields().calendar, "iso8601", `Calendar created from string "${arg}"`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-iso-string.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-iso-string.js
new file mode 100644
index 0000000000..a0b1fa2928
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-iso-string.js
@@ -0,0 +1,29 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: An ISO 8601 string can be converted to a calendar ID in Calendar
+features: [Temporal]
+---*/
+
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, timeZone);
+
+for (const calendar of [
+ "2020-01-01",
+ "2020-01-01[u-ca=iso8601]",
+ "2020-01-01T00:00:00.000000000",
+ "2020-01-01T00:00:00.000000000[u-ca=iso8601]",
+ "01-01",
+ "01-01[u-ca=iso8601]",
+ "2020-01",
+ "2020-01[u-ca=iso8601]",
+]) {
+ const arg = { year: 1976, monthCode: "M11", day: 18, calendar };
+ const result = instance.withPlainDate(arg);
+ assert.sameValue(result.epochNanoseconds, 217_129_600_000_000_000n, `Calendar created from string "${calendar}"`);
+}
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-year-zero.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-year-zero.js
index 3d7d95215e..52cefeffa3 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-year-zero.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-propertybag-calendar-year-zero.js
@@ -17,7 +17,8 @@ const invalidStrings = [
];
const timeZone = new Temporal.TimeZone("UTC");
const instance = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, timeZone);
-invalidStrings.forEach((arg) => {
+invalidStrings.forEach((str) => {
+ const arg = { year: 1976, month: 11, day: 18, calendar: str };
assert.throws(
RangeError,
() => instance.withPlainDate(arg),
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..c1f874813b
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,26 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["1970-01-01[U-CA=iso8601]", "invalid capitalized key"],
+ ["1970-01-01[u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["1970-01-01[FOO=bar]", "invalid capitalized unrecognized key"],
+];
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => instance.withPlainDate(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..d595cb1952
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.withPlainDate(new Temporal.PlainDate(1970, 1, 1));
+
+assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..7af0ddc2d1
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
@@ -0,0 +1,44 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.withPlainDate(new Temporal.PlainDate(1970, 1, 1)), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..2ed4c8f1d9
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,53 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.withPlainDate(new Temporal.PlainDate(1970, 1, 1));
+
+assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..b43bdba300
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainDate/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaindate
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.withPlainDate(new Temporal.PlainDate(1970, 1, 1)), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/argument-string-calendar-annotation-invalid-key.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/argument-string-calendar-annotation-invalid-key.js
new file mode 100644
index 0000000000..d07df63e1e
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/argument-string-calendar-annotation-invalid-key.js
@@ -0,0 +1,32 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaintime
+description: Annotation keys are lowercase-only
+features: [Temporal]
+---*/
+
+const invalidStrings = [
+ ["00:00[U-CA=iso8601]", "invalid capitalized key, time-only"],
+ ["T00:00[U-CA=iso8601]", "invalid capitalized key, time designator"],
+ ["1970-01-01T00:00[U-CA=iso8601]", "invalid capitalized key"],
+ ["00:00[u-CA=iso8601]", "invalid partially-capitalized key, time-only"],
+ ["T00:00[u-CA=iso8601]", "invalid partially-capitalized key, time designator"],
+ ["1970-01-01T00:00[u-CA=iso8601]", "invalid partially-capitalized key"],
+ ["00:00[FOO=bar]", "invalid capitalized unrecognized key, time-only"],
+ ["T00:00[FOO=bar]", "invalid capitalized unrecognized key, time designator"],
+ ["1970-01-01T00:00[FOO=bar]", "invalid capitalized unrecognized key"],
+];
+const timeZone = new Temporal.TimeZone("UTC");
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+invalidStrings.forEach(([arg, descr]) => {
+ assert.throws(
+ RangeError,
+ () => instance.withPlainTime(arg),
+ `annotation keys must be lowercase: ${arg} - ${descr}`
+ );
+});
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-maximum-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
new file mode 100644
index 0000000000..0bf0721c54
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-maximum-forward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaintime
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants can be at most 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ calls++;
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12 })];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.withPlainTime();
+
+assert(calls >= 2, "getOffsetNanosecondsFor should be called at least twice");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
new file mode 100644
index 0000000000..8f2615faae
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getoffsetnanosecondsfor-out-of-range-forward-offset-shift.js
@@ -0,0 +1,44 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaintime
+description: >
+ UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor
+ in DisambiguatePossibleInstants cannot be greater than 24 hours.
+features: [Temporal]
+info: |
+ DisambiguatePossibleInstants:
+ 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+ _shiftEpochNs = 0n
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9;
+ return 12 * 3600e9 + 1;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const [utcInstant] = super.getPossibleInstantsFor(plainDateTime);
+ const { year, month, day } = plainDateTime;
+
+ if (year < 1970) return [utcInstant.subtract({ hours: 12 })];
+ if (year === 1970 && month === 1 && day === 1) return [];
+ return [utcInstant.add({ hours: 12, nanoseconds: 1 })];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.withPlainTime(), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-maximum-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-maximum-backward-offset-shift.js
new file mode 100644
index 0000000000..21dcd8b3ba
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-maximum-backward-offset-shift.js
@@ -0,0 +1,53 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaintime
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+let calls = 0;
+
+class Shift24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ calls++;
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12 }),
+ utcInstant.add({ hours: 12 })
+ ];
+ }
+}
+
+const timeZone = new Shift24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+instance.withPlainTime();
+
+assert(calls >= 1, "getPossibleInstantsFor should be called at least once");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
new file mode 100644
index 0000000000..f2856f0aea
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/withPlainTime/getpossibleinstantsfor-out-of-range-backward-offset-shift.js
@@ -0,0 +1,49 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.withplaintime
+description: >
+ UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours.
+features: [Temporal]
+info: |
+ GetPossibleInstantsFor:
+ 5.b.i. Let _numResults_ be _list_'s length.
+ ii. If _numResults_ > 1, then
+ 1. Let _epochNs_ be a new empty List.
+ 2. For each value _instant_ in list, do
+ a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_.
+ 3. Let _min_ be the least element of the List _epochNs_.
+ 4. Let _max_ be the greatest element of the List _epochNs_.
+ 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception.
+---*/
+
+class ShiftLonger24Hour extends Temporal.TimeZone {
+ id = 'TestTimeZone';
+
+ constructor() {
+ super('UTC');
+ }
+
+ getOffsetNanosecondsFor(instant) {
+ return 0;
+ }
+
+ getPossibleInstantsFor(plainDateTime) {
+ const utc = new Temporal.TimeZone("UTC");
+ const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime);
+ return [
+ utcInstant.subtract({ hours: 12, nanoseconds: 1 }),
+ utcInstant.add({ hours: 12 }),
+ utcInstant, // add a third value in case the implementation doesn't sort
+ ];
+ }
+}
+
+const timeZone = new ShiftLonger24Hour();
+
+const instance = new Temporal.ZonedDateTime(0n, timeZone);
+assert.throws(RangeError, () => instance.withPlainTime(), "RangeError should be thrown");
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/custom-calendar-weekofyear.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/custom-calendar-weekofyear.js
new file mode 100644
index 0000000000..331002f262
--- /dev/null
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/custom-calendar-weekofyear.js
@@ -0,0 +1,27 @@
+// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally
+// Copyright (C) 2024 Igalia, S.L. All rights reserved.
+// This code is governed by the BSD license found in the LICENSE file.
+
+/*---
+esid: sec-temporal.zoneddatetime.prototype.yearofweek
+description: >
+ Temporal.ZonedDateTime.prototype.yearOfWeek returns undefined for all
+ custom calendars where yearOfWeek() returns undefined.
+features: [Temporal]
+---*/
+
+class CustomCalendar extends Temporal.Calendar {
+ constructor() {
+ super("iso8601");
+ }
+ yearOfWeek() {
+ return undefined;
+ }
+}
+
+const calendar = new CustomCalendar();
+// Epoch Nanoseconds for new Temporal.PlainDateTime(2024, 1, 1, 12, 34, 56, 987, 654, 321, calendar);
+const customCalendarDate = new Temporal.ZonedDateTime(1_704_112_496_987_654_321n, "UTC", calendar);
+assert.sameValue(customCalendarDate.yearOfWeek, undefined);
+
+reportCompare(0, 0);
diff --git a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/validate-calendar-value.js b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/validate-calendar-value.js
index 9bd447f9a1..9eb067c70b 100644
--- a/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/validate-calendar-value.js
+++ b/js/src/tests/test262/built-ins/Temporal/ZonedDateTime/prototype/yearOfWeek/validate-calendar-value.js
@@ -9,7 +9,6 @@ features: [Temporal]
---*/
const badResults = [
- [undefined, TypeError],
[Infinity, RangeError],
[-Infinity, RangeError],
[Symbol("foo"), TypeError],