Temporal: Fast-path dateUntil() when difference largest unit is days

This removes several loopholes where it was possible to return particular
values from user calls that would cause infinite loops, or calculate
zero-length days.
This commit is contained in:
Philip Chimento 2023-03-09 17:39:19 -08:00 committed by Philip Chimento
parent 7a3944c0cc
commit 9b8d9cf66c
50 changed files with 538 additions and 702 deletions

View File

@ -46,13 +46,13 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"],
hours: ["day"],
minutes: ["day"],
seconds: ["day"],
milliseconds: ["day"],
microseconds: ["day"],
nanoseconds: ["day"]
days: [],
hours: [],
minutes: [],
seconds: [],
milliseconds: [],
microseconds: [],
nanoseconds: []
}
);
@ -64,10 +64,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
one.add(two, { relativeTo, largestUnit });
},
{
years: ["year", "day"],
months: ["month", "day"],
weeks: ["week", "day"],
days: ["day", "day"],
years: ["year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],

View File

@ -1,58 +0,0 @@
// Copyright (C) 2022 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.add
description: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const duration = Temporal.Duration.from({ days: 1 });
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the second call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
TemporalHelpers.SUBSTITUTE_SKIP,
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
duration.add(duration, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
200 + 2,
"Expected duration.add to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
duration.add(duration, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
300 + 2,
"Expected duration.add to call getPossibleInstantsFor correct number of times"
);

View File

@ -229,9 +229,6 @@ const expectedOpsForPlainRelativeToNoCalendarOperations = [
// InterpretTemporalDateTimeFields
"get options.relativeTo.calendar.dateFromFields",
"call options.relativeTo.calendar.dateFromFields",
// AddDuration
"get options.relativeTo.calendar.dateUntil",
"call options.relativeTo.calendar.dateUntil",
];
const noCalendarInstance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321);
@ -355,9 +352,6 @@ const expectedOpsForZonedRelativeTo = expected.concat([
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor",
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → DifferenceISODateTime
"get options.relativeTo.calendar.dateUntil",
"call options.relativeTo.calendar.dateUntil",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 1
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",

View File

@ -76,13 +76,13 @@ features: [Temporal]
---*/
// Check with smallestUnit nanoseconds but roundingIncrement > 1; each call
// should result in two calls to dateUntil() originating from
// AdjustRoundedDurationDays, one with largestUnit equal to the largest unit in
// the duration higher than "day", and one with largestUnit: "day".
// should result in one call to dateUntil() originating from
// AdjustRoundedDurationDays, with largestUnit equal to the largest unit in
// the duration higher than "day".
// Additionally one call with largestUnit: "month" in BalanceDurationRelative
// when the largestUnit given to round() is "year", and one call with
// largestUnit: "day" when the largestUnit given to round() is "year", "month",
// "week", or "day".
// when the largestUnit given to round() is "year".
// Other calls have largestUnit: "day" so the difference is taken in ISO
// calendar space.
const durations = [
[1, 0, 0, 0, 0, 0, 0, 0, 0, 86399_999_999_999],
@ -104,16 +104,16 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
duration.round({ largestUnit, roundingIncrement: 2, roundingMode: 'ceil', relativeTo });
},
{
years: ["year", "day", "day", "month"],
months: ["month", "day", "day"],
weeks: ["week", "day", "day"],
days: ["day", "day", "day"],
hours: ["day", "day"],
minutes: ["day", "day"],
seconds: ["day", "day"],
milliseconds: ["day", "day"],
microseconds: ["day", "day"],
nanoseconds: ["day", "day"]
years: ["year", "month"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],
milliseconds: [],
microseconds: [],
nanoseconds: []
}
);
@ -142,18 +142,18 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
// Check the paths that call dateUntil() in RoundDuration. These paths do not
// call dateUntil() in AdjustRoundedDurationDays. Note that there is no
// largestUnit: "month" call in BalanceDurationRelative and no largestUnit:
// "day" call in BalanceDuration, because the durations have rounded down to 0.
// largestUnit: "month" call in BalanceDurationRelative, because the durations
// have rounded down to 0.
TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
(calendar, largestUnit) => {
const duration = new Temporal.Duration(0, 0, 0, 0, 1, 1, 1, 1, 1, 1);
const duration = new Temporal.Duration(0, 1, 0, 0, 1, 1, 1, 1, 1, 1);
const relativeTo = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar);
duration.round({ largestUnit, smallestUnit: largestUnit, relativeTo });
}, {
years: ["day", "year"],
months: ["day"],
weeks: ["day"],
days: ["day"]
years: ["year"],
months: [],
weeks: [],
days: []
}
);

View File

@ -1,58 +0,0 @@
// Copyright (C) 2022 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.round
description: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const duration = Temporal.Duration.from({ days: 1 });
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the first call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
duration.round({
largestUnit: "days",
relativeTo: zdt,
});
assert.sameValue(
calls.length,
200 + 2,
"Expected duration.round to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
duration.round({
largestUnit: "days",
relativeTo: zdt,
});
assert.sameValue(
calls.length,
300 + 2,
"Expected duration.round to call getPossibleInstantsFor correct number of times"
);

View File

@ -380,8 +380,6 @@ const expectedOpsForMinimalYearRoundingZoned = expectedOpsForZonedRelativeTo.con
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.calendar.dateUntil", // 12. DifferenceISODateTime
"call options.relativeTo.calendar.dateUntil",
// NanosecondsToDays → AddDaysToZonedDateTime
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",
@ -421,8 +419,6 @@ const expectedOpsForYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.calendar.dateUntil", // 12. DifferenceISODateTime
"call options.relativeTo.calendar.dateUntil",
// NanosecondsToDays → AddDaysToZonedDateTime
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",

View File

@ -1,53 +0,0 @@
// Copyright (C) 2023 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.round
description: A malicious time zone resulting a day length of zero is handled correctly
info: |
Based on a test by André Bargull.
RoundDuration step 6:
d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_).
e. Set _days_ to _days_ + _result_.[[Days]] + _result_.[[Nanoseconds]] / _result_.[[DayLength]].
NanosecondsToDays steps 19-23:
19. If _days_ < 0 and _sign_ = 1, throw a *RangeError* exception.
20. If _days_ > 0 and _sign_ = -1, throw a *RangeError* exception.
21. If _nanoseconds_ < 0, then
a. Assert: sign is -1.
22. If _nanoseconds_ > 0 and _sign_ = -1, throw a *RangeError* exception.
23. Assert: The inequality abs(_nanoseconds_) < abs(_dayLengthNs_) holds.
features: [Temporal]
---*/
const instance = new Temporal.Duration(0, 0, 0, 0, 24, 0, 0, 0, 0, 1);
const tz = new class extends Temporal.TimeZone {
#getPossibleInstantsForCalls = 0;
getPossibleInstantsFor(dt) {
this.#getPossibleInstantsForCalls++;
if (this.#getPossibleInstantsForCalls <= 2) {
return [new Temporal.Instant(86400_000_000_000n + 2n)]
}
return super.getPossibleInstantsFor(dt);
}
}("UTC");
const cal = new class extends Temporal.Calendar {
#dateUntilCalls = 0;
dateUntil(one, two, options) {
this.#dateUntilCalls++;
if (this.#dateUntilCalls === 1) {
return new Temporal.Duration(0, 0, 0, -2);
}
return super.dateUntil(one, two, options);
}
}("iso8601");
const relativeTo = new Temporal.ZonedDateTime(0n, tz, cal);
assert.throws(RangeError, () => instance.round({ relativeTo, smallestUnit: "days" }));

View File

@ -46,13 +46,13 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"],
hours: ["day"],
minutes: ["day"],
seconds: ["day"],
milliseconds: ["day"],
microseconds: ["day"],
nanoseconds: ["day"]
days: [],
hours: [],
minutes: [],
seconds: [],
milliseconds: [],
microseconds: [],
nanoseconds: []
}
);
@ -64,10 +64,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
two.subtract(one, { relativeTo });
},
{
years: ["year", "day"],
months: ["month", "day"],
weeks: ["week", "day"],
days: ["day", "day"],
years: ["year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],

View File

@ -1,59 +0,0 @@
// Copyright (C) 2022 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.subtract
description: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const duration = Temporal.Duration.from({ days: 1 });
const other = Temporal.Duration.from({ hours: 1 });
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the second call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
TemporalHelpers.SUBSTITUTE_SKIP,
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
duration.subtract(other, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
200 + 2,
"Expected duration.subtract to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
duration.subtract(other, {
relativeTo: zdt,
});
assert.sameValue(
calls.length,
300 + 2,
"Expected duration.subtract to call getPossibleInstantsFor correct number of times"
);

View File

@ -229,9 +229,6 @@ const expectedOpsForPlainRelativeToNoCalendarOperations = [
// InterpretTemporalDateTimeFields
"get options.relativeTo.calendar.dateFromFields",
"call options.relativeTo.calendar.dateFromFields",
// AddDuration
"get options.relativeTo.calendar.dateUntil",
"call options.relativeTo.calendar.dateUntil",
];
const noCalendarInstance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321);
@ -355,9 +352,6 @@ const expectedOpsForZonedRelativeTo = expected.concat([
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor",
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → DifferenceISODateTime
"get options.relativeTo.calendar.dateUntil",
"call options.relativeTo.calendar.dateUntil",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 1
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",

View File

@ -44,10 +44,9 @@ includes: [compareArray.js, temporalHelpers.js]
features: [Temporal]
---*/
// Check the paths that go through NanosecondsToDays: one call to dateUntil() in
// BalanceDuration and one in RoundDuration with largestUnit: "day" when the
// unit is "year", "month", "week", or "day", and one extra call with
// largestUnit: "year" in RoundDuration when the unit is "year".
// Check the paths that go through NanosecondsToDays: only one call with
// largestUnit: "year" in RoundDuration when the unit is "year". The others all
// have largestUnit: "day" so the difference is taken in ISO calendar space.
const duration = new Temporal.Duration(0, 1, 1, 1, 1, 1, 1, 1, 1, 1);
@ -57,10 +56,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
duration.total({ unit, relativeTo });
},
{
years: ["day", "day", "year"],
months: ["day", "day"],
weeks: ["day", "day"],
days: ["day", "day"],
years: ["year"],
months: [],
weeks: [],
days: [],
hours: [],
minutes: [],
seconds: [],
@ -74,7 +73,7 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
(calendar, unit) => {
const duration = new Temporal.Duration(5);
const duration = new Temporal.Duration(5, 1);
const relativeTo = new Temporal.PlainDateTime(2000, 5, 2, 0, 0, 0, 0, 0, 0, calendar);
duration.total({ unit, relativeTo });
},

View File

@ -1,59 +0,0 @@
// Copyright (C) 2022 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.total
description: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const duration = Temporal.Duration.from({ days: 1 });
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the first call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
duration.total({
unit: "day",
relativeTo: zdt,
});
assert.sameValue(
calls.length,
200 + 3,
"Expected duration.total to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
duration.total({
unit: "day",
relativeTo: zdt,
});
assert.sameValue(
calls.length,
300 + 3,
"Expected duration.total to call getPossibleInstantsFor correct number of times"
);

View File

@ -281,8 +281,6 @@ const expectedOpsForMinimalYearRoundingZoned = expectedOpsForZonedRelativeTo.con
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.calendar.dateUntil", // 12. DifferenceISODateTime
"call options.relativeTo.calendar.dateUntil",
// BalancePossiblyInfiniteDuration → NanosecondsToDays → AddDaysToZonedDateTime
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",
@ -328,8 +326,6 @@ const expectedOpsForYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"get options.relativeTo.calendar.dateUntil", // 12. DifferenceISODateTime
"call options.relativeTo.calendar.dateUntil",
// BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays → AddDaysToZonedDateTime
"get options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.timeZone.getPossibleInstantsFor",

View File

@ -1,53 +0,0 @@
// Copyright (C) 2023 Igalia, S.L. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.duration.prototype.round
description: A malicious time zone resulting a day length of zero is handled correctly
info: |
Based on a test by André Bargull.
RoundDuration step 6:
d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_).
e. Set _days_ to _days_ + _result_.[[Days]] + _result_.[[Nanoseconds]] / _result_.[[DayLength]].
NanosecondsToDays steps 19-23:
19. If _days_ < 0 and _sign_ = 1, throw a *RangeError* exception.
20. If _days_ > 0 and _sign_ = -1, throw a *RangeError* exception.
21. If _nanoseconds_ < 0, then
a. Assert: sign is -1.
22. If _nanoseconds_ > 0 and _sign_ = -1, throw a *RangeError* exception.
23. Assert: The inequality abs(_nanoseconds_) < abs(_dayLengthNs_) holds.
features: [Temporal]
---*/
const instance = new Temporal.Duration(0, 0, 0, 0, 24, 0, 0, 0, 0, 1);
const tz = new class extends Temporal.TimeZone {
#getPossibleInstantsForCalls = 0;
getPossibleInstantsFor(dt) {
this.#getPossibleInstantsForCalls++;
if (this.#getPossibleInstantsForCalls <= 2) {
return [new Temporal.Instant(86400_000_000_000n + 2n)]
}
return super.getPossibleInstantsFor(dt);
}
}("UTC");
const cal = new class extends Temporal.Calendar {
#dateUntilCalls = 0;
dateUntil(one, two, options) {
this.#dateUntilCalls++;
if (this.#dateUntilCalls === 1) {
return new Temporal.Duration(0, 0, 0, -2);
}
return super.dateUntil(one, two, options);
}
}("iso8601");
const relativeTo = new Temporal.ZonedDateTime(0n, tz, cal);
assert.throws(RangeError, () => instance.total({ relativeTo, unit: "days" }));

View File

@ -13,5 +13,5 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.PlainDate(2000, 5, 2, calendar);
const argument = new Temporal.PlainDate(2022, 6, 14, calendar);
instance.since(argument);
instance.since(argument, { largestUnit: "months" });
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -22,6 +22,6 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"]
days: []
}
);

View File

@ -32,5 +32,9 @@ const tests = [
];
for (const [test, description = typeof test] of tests) {
const plainDate = new Temporal.PlainDate(2000, 5, 2, new CustomCalendar(test));
assert.throws(TypeError, () => plainDate.since("2022-06-20"), `Expected error with ${description}`);
assert.throws(
TypeError,
() => plainDate.since("2022-06-20", { largestUnit: "years" }),
`Expected error with ${description}`
);
}

View File

@ -9,7 +9,7 @@ features: [Temporal]
---*/
const result = new Temporal.Duration(1, 3, 5, 7, 9);
const options = {};
const options = { largestUnit: "years" };
let calls = 0;
class CustomCalendar extends Temporal.Calendar {
constructor() {
@ -20,7 +20,7 @@ class CustomCalendar extends Temporal.Calendar {
assert.sameValue(args.length, 3, "Three arguments");
assert.sameValue(args[0], plainDate, "First argument");
assert.sameValue(args[1], other, "Second argument");
assert.sameValue(args[2].largestUnit, "day", "Third argument: largestUnit");
assert.sameValue(args[2].largestUnit, "year", "Third argument: largestUnit");
return result;
}
}

View File

@ -86,8 +86,8 @@ const instance = new Temporal.PlainDate(2000, 5, 2, ownCalendar);
const otherDatePropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: 'M05',
month: 6,
monthCode: "M06",
day: 2,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
@ -107,8 +107,8 @@ function createOptionsObserver({ smallestUnit = "days", largestUnit = "auto", ro
// clear any observable things that happened while constructing the objects
actual.splice(0);
// basic order of observable operations, without rounding:
instance.since(otherDatePropertyBag, createOptionsObserver());
// basic order of observable operations with calendar call, without rounding:
instance.since(otherDatePropertyBag, createOptionsObserver({ largestUnit: "years" }));
assert.compareArray(actual, expected, "order of operations");
actual.splice(0); // clear
@ -138,6 +138,24 @@ instance.since(otherDatePropertyBag, createOptionsObserver({ smallestUnit: "year
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call:
const otherDatePropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
day: 2,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.since(otherDatePropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years and no excess months/weeks");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -13,5 +13,5 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.PlainDate(2000, 5, 2, calendar);
const argument = new Temporal.PlainDate(2022, 6, 14, calendar);
instance.until(argument);
instance.until(argument, { largestUnit: "months" });
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -22,6 +22,6 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"]
days: []
}
);

View File

@ -32,5 +32,8 @@ const tests = [
];
for (const [test, description = typeof test] of tests) {
const plainDate = new Temporal.PlainDate(2000, 5, 2, new CustomCalendar(test));
assert.throws(TypeError, () => plainDate.until("2022-06-20"), `Expected error with ${description}`);
assert.throws(
TypeError, () => plainDate.until("2022-06-20", { largestUnit: "years" }),
`Expected error with ${description}`
);
}

View File

@ -9,7 +9,7 @@ features: [Temporal]
---*/
const result = new Temporal.Duration(1, 3, 5, 7, 9);
const options = {};
const options = { largestUnit: "years" };
let calls = 0;
class CustomCalendar extends Temporal.Calendar {
constructor() {
@ -20,7 +20,7 @@ class CustomCalendar extends Temporal.Calendar {
assert.sameValue(args.length, 3, "Three arguments");
assert.sameValue(args[0], plainDate, "First argument");
assert.sameValue(args[1], other, "Second argument");
assert.sameValue(args[2].largestUnit, "day", "Third argument: largestUnit");
assert.sameValue(args[2].largestUnit, "year", "Third argument: largestUnit");
return result;
}
}

View File

@ -86,8 +86,8 @@ const instance = new Temporal.PlainDate(2000, 5, 2, ownCalendar);
const otherDatePropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
month: 6,
monthCode: "M06",
day: 2,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
@ -107,8 +107,8 @@ function createOptionsObserver({ smallestUnit = "days", largestUnit = "auto", ro
// clear any observable things that happened while constructing the objects
actual.splice(0);
// basic order of observable operations, without rounding:
instance.until(otherDatePropertyBag, createOptionsObserver());
// basic order of observable operations with calendar call, without rounding:
instance.until(otherDatePropertyBag, createOptionsObserver({ largestUnit: "years" }));
assert.compareArray(actual, expected, "order of operations");
actual.splice(0); // clear
@ -139,6 +139,24 @@ instance.until(otherDatePropertyBag, createOptionsObserver({ smallestUnit: "year
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call:
const otherDatePropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
day: 2,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.until(otherDatePropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years and no excess months/weeks");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -1,44 +0,0 @@
// Copyright (C) 2022 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.plaindatetime.prototype.since
description: >
BalanceDuration throws when the result duration is invalid.
info: |
DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1,
y2, mon2, d2, h2, min2, s2, ms2, mus2, ns2,
calendar, largestUnit, options )
...
12. Let dateDifference be ? CalendarDateUntil(calendar, date1, date2, untilOptions).
13. Let balanceResult be ? BalanceDuration(dateDifference.[[Days]], timeDifference.[[Hours]],
timeDifference.[[Minutes]], timeDifference.[[Seconds]], timeDifference.[[Milliseconds]],
timeDifference.[[Microseconds]], timeDifference.[[Nanoseconds]], largestUnit).
...
BalanceDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds,
largestUnit [ , relativeTo ] )
...
3. Else,
a. Set nanoseconds to ! TotalDurationNanoseconds(days, hours, minutes, seconds, milliseconds,
microseconds, nanoseconds, 0).
...
15. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign,
milliseconds × sign, microseconds × sign, nanoseconds × sign).
features: [Temporal]
---*/
var cal = new class extends Temporal.Calendar {
dateUntil(date1, date2, options) {
return Temporal.Duration.from({days: Number.MAX_VALUE});
}
}("iso8601");
var dt1 = new Temporal.PlainDateTime(1970, 1, 1, 0, 0, 0, 0, 0, 0, cal);
var dt2 = new Temporal.PlainDateTime(1970, 1, 2, 0, 0, 0, 0, 0, 0, cal);
var options = {largestUnit: "nanoseconds"};
assert.throws(RangeError, () => dt1.since(dt2, options));
assert.throws(RangeError, () => dt2.since(dt1, options));

View File

@ -13,5 +13,5 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar);
const argument = new Temporal.PlainDateTime(2022, 6, 14, 18, 21, 36, 660, 690, 387, calendar);
instance.since(argument);
instance.since(argument, { largestUnit: "months" });
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -25,12 +25,12 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"],
hours: ["day"],
minutes: ["day"],
seconds: ["day"],
milliseconds: ["day"],
microseconds: ["day"],
nanoseconds: ["day"]
days: [],
hours: [],
minutes: [],
seconds: [],
milliseconds: [],
microseconds: [],
nanoseconds: []
}
);

View File

@ -131,8 +131,8 @@ function createOptionsObserver({ smallestUnit = "nanoseconds", largestUnit = "au
// clear any observable things that happened while constructing the objects
actual.splice(0);
// basic order of observable operations, without rounding:
instance.since(otherDateTimePropertyBag, createOptionsObserver());
// basic order of observable operations with calendar call, without rounding:
instance.since(otherDateTimePropertyBag, createOptionsObserver({ largestUnit: "years" }));
assert.compareArray(actual, expected, "order of operations");
actual.splice(0); // clear
@ -168,6 +168,30 @@ instance.since(otherDateTimePropertyBag, createOptionsObserver({ smallestUnit: "
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call:
const otherDatePropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
day: 2,
hour: 12,
minute: 34,
second: 56,
millisecond: 987,
microsecond: 654,
nanosecond: 321,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.until(otherDatePropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years and no excess months/weeks");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -1,44 +0,0 @@
// Copyright (C) 2022 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-temporal.plaindatetime.prototype.since
description: >
BalanceDuration throws when the result duration is invalid.
info: |
DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1,
y2, mon2, d2, h2, min2, s2, ms2, mus2, ns2,
calendar, largestUnit, options )
...
12. Let dateDifference be ? CalendarDateUntil(calendar, date1, date2, untilOptions).
13. Let balanceResult be ? BalanceDuration(dateDifference.[[Days]], timeDifference.[[Hours]],
timeDifference.[[Minutes]], timeDifference.[[Seconds]], timeDifference.[[Milliseconds]],
timeDifference.[[Microseconds]], timeDifference.[[Nanoseconds]], largestUnit).
...
BalanceDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds,
largestUnit [ , relativeTo ] )
...
3. Else,
a. Set nanoseconds to ! TotalDurationNanoseconds(days, hours, minutes, seconds, milliseconds,
microseconds, nanoseconds, 0).
...
15. Return ? CreateTimeDurationRecord(days, hours × sign, minutes × sign, seconds × sign,
milliseconds × sign, microseconds × sign, nanoseconds × sign).
features: [Temporal]
---*/
var cal = new class extends Temporal.Calendar {
dateUntil(date1, date2, options) {
return Temporal.Duration.from({days: Number.MAX_VALUE});
}
}("iso8601");
var dt1 = new Temporal.PlainDateTime(1970, 1, 1, 0, 0, 0, 0, 0, 0, cal);
var dt2 = new Temporal.PlainDateTime(1970, 1, 2, 0, 0, 0, 0, 0, 0, cal);
var options = {largestUnit: "nanoseconds"};
assert.throws(RangeError, () => dt1.until(dt2, options));
assert.throws(RangeError, () => dt2.until(dt1, options));

View File

@ -13,5 +13,5 @@ features: [Temporal]
const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.PlainDateTime(2000, 5, 2, 12, 34, 56, 987, 654, 321, calendar);
const argument = new Temporal.PlainDateTime(2022, 6, 14, 18, 21, 36, 660, 690, 387, calendar);
instance.until(argument);
instance.until(argument, { largestUnit: "months" });
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -25,12 +25,12 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
years: ["year"],
months: ["month"],
weeks: ["week"],
days: ["day"],
hours: ["day"],
minutes: ["day"],
seconds: ["day"],
milliseconds: ["day"],
microseconds: ["day"],
nanoseconds: ["day"]
days: [],
hours: [],
minutes: [],
seconds: [],
milliseconds: [],
microseconds: [],
nanoseconds: []
}
);

View File

@ -131,8 +131,8 @@ function createOptionsObserver({ smallestUnit = "nanoseconds", largestUnit = "au
// clear any observable things that happened while constructing the objects
actual.splice(0);
// basic order of observable operations, without rounding:
instance.until(otherDateTimePropertyBag, createOptionsObserver());
// basic order of observable operations with calendar call, without rounding:
instance.until(otherDateTimePropertyBag, createOptionsObserver({ largestUnit: "years" }));
assert.compareArray(actual, expected, "order of operations");
actual.splice(0); // clear
@ -168,6 +168,30 @@ instance.until(otherDateTimePropertyBag, createOptionsObserver({ smallestUnit: "
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call:
const otherDatePropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
day: 2,
hour: 12,
minute: 34,
second: 56,
millisecond: 987,
microsecond: 654,
nanosecond: 321,
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.until(otherDatePropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years and no excess months/weeks");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -100,8 +100,8 @@ const instance = new Temporal.PlainYearMonth(2000, 5, ownCalendar, 1);
const otherYearMonthPropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
month: 6,
monthCode: "M06",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
@ -150,6 +150,23 @@ instance.since(otherYearMonthPropertyBag, createOptionsObserver({ smallestUnit:
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call
const otherYearMonthPropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.since(otherYearMonthPropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -100,8 +100,8 @@ const instance = new Temporal.PlainYearMonth(2000, 5, ownCalendar, 1);
const otherYearMonthPropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
month: 6,
monthCode: "M06",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
@ -150,6 +150,23 @@ instance.until(otherYearMonthPropertyBag, createOptionsObserver({ smallestUnit:
assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest year and skips a DateUntil call
const otherYearMonthPropertyBagSameMonth = TemporalHelpers.propertyBagObserver(actual, {
year: 2001,
month: 5,
monthCode: "M05",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
}, "other");
const expectedOpsForYearRoundingSameMonth = expected.concat([
"get this.calendar.dateAdd", // 7.c.i
"call this.calendar.dateAdd", // 7.e
"call this.calendar.dateAdd", // 7.g
"call this.calendar.dateAdd", // 7.y MoveRelativeDate
]); // (7.o not called because months and weeks == 0)
instance.until(otherYearMonthPropertyBagSameMonth, createOptionsObserver({ smallestUnit: "years" }));
assert.compareArray(actual, expectedOpsForYearRoundingSameMonth, "order of operations with smallestUnit = years");
actual.splice(0); // clear
// code path through RoundDuration that rounds to the nearest month:
const expectedOpsForMonthRounding = expected.concat([
"get this.calendar.dateAdd", // 10.b

View File

@ -14,4 +14,4 @@ const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.ZonedDateTime(0n, "UTC", calendar);
const argument = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar);
instance.since(argument, { largestUnit: "year" });
assert.sameValue(calendar.dateUntilCallCount, 2, "dateUntil should have been called on the calendar");
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -55,10 +55,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
later.since(earlier, { largestUnit });
},
{
years: ["year", "day"],
months: ["month", "day"],
weeks: ["week", "day"],
days: ["day", "day"],
years: ["year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],
@ -72,15 +72,15 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
(calendar, largestUnit) => {
const earlier = new Temporal.ZonedDateTime(0n, "UTC", calendar);
const earlier = new Temporal.ZonedDateTime(-31536000_000_000_000n /* = -365 days */, "UTC", calendar);
const later = new Temporal.ZonedDateTime(86_399_999_999_999n, "UTC", calendar);
later.since(earlier, { largestUnit, roundingIncrement: 2, roundingMode: 'ceil' });
},
{
years: ["year", "day", "day", "day"],
months: ["month", "day", "day", "day"],
weeks: ["week", "day", "day", "day"],
days: ["day", "day", "day", "day"],
years: ["year", "year"],
months: ["month", "month"],
weeks: ["week", "week"],
days: [],
hours: [],
minutes: [],
seconds: [],
@ -100,10 +100,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
later.since(earlier, { smallestUnit });
},
{
years: ["year", "day", "day", "year"],
months: ["month", "day", "day"],
weeks: ["week", "day", "day"],
days: ["day", "day", "day"],
years: ["year", "year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],

View File

@ -1,59 +0,0 @@
// Copyright (C) 2022 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: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const dayLengthNs = 86400000000000n;
const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the second call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
TemporalHelpers.SUBSTITUTE_SKIP,
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
zdt.since(other, {
largestUnit: "day",
});
assert.sameValue(
calls.length,
200 + 1,
"Expected ZonedDateTime.since to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
zdt.since(other, {
largestUnit: "day",
});
assert.sameValue(
calls.length,
300 + 1,
"Expected ZonedDateTime.since to call getPossibleInstantsFor correct number of times"
);

View File

@ -108,6 +108,18 @@ const ownTimeZone = TemporalHelpers.timeZoneObserver(actual, "this.timeZone");
const ownCalendar = TemporalHelpers.calendarObserver(actual, "this.calendar");
const instance = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, ownTimeZone, ownCalendar);
const dstTimeZone = TemporalHelpers.springForwardFallBackTimeZone();
const ownDstTimeZone = TemporalHelpers.timeZoneObserver(actual, "this.timeZone", {
getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor,
getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor,
});
const otherDstTimeZone = TemporalHelpers.timeZoneObserver(actual, "other.timeZone", {
getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor,
getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor,
});
/* 2000-10-29T01:30-07:00, in the middle of the first repeated hour: */
const fallBackInstance = new Temporal.ZonedDateTime(972808200_000_000_000n, ownDstTimeZone, ownCalendar);
const otherDateTimePropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2004,
month: 5,
@ -169,6 +181,139 @@ assert.compareArray(actual, expected.concat([
]), "order of operations with identical dates and largestUnit a calendar unit");
actual.splice(0); // clear
// two ZonedDateTimes that denote the same wall-clock time in the time zone can
// avoid calling some calendar methods:
const fallBackPropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2000,
month: 10,
monthCode: "M10",
day: 29,
hour: 1,
minute: 30,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0,
offset: "-08:00",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
timeZone: otherDstTimeZone,
}, "other");
fallBackInstance.since(fallBackPropertyBag, createOptionsObserver({ largestUnit: "days" }));
assert.compareArray(actual, [
// ToTemporalZonedDateTime
"get other.calendar",
"has other.calendar.dateAdd",
"has other.calendar.dateFromFields",
"has other.calendar.dateUntil",
"has other.calendar.day",
"has other.calendar.dayOfWeek",
"has other.calendar.dayOfYear",
"has other.calendar.daysInMonth",
"has other.calendar.daysInWeek",
"has other.calendar.daysInYear",
"has other.calendar.fields",
"has other.calendar.id",
"has other.calendar.inLeapYear",
"has other.calendar.mergeFields",
"has other.calendar.month",
"has other.calendar.monthCode",
"has other.calendar.monthDayFromFields",
"has other.calendar.monthsInYear",
"has other.calendar.weekOfYear",
"has other.calendar.year",
"has other.calendar.yearMonthFromFields",
"has other.calendar.yearOfWeek",
"get other.calendar.fields",
"call other.calendar.fields",
"get other.day",
"get other.day.valueOf",
"call other.day.valueOf",
"get other.hour",
"get other.hour.valueOf",
"call other.hour.valueOf",
"get other.microsecond",
"get other.microsecond.valueOf",
"call other.microsecond.valueOf",
"get other.millisecond",
"get other.millisecond.valueOf",
"call other.millisecond.valueOf",
"get other.minute",
"get other.minute.valueOf",
"call other.minute.valueOf",
"get other.month",
"get other.month.valueOf",
"call other.month.valueOf",
"get other.monthCode",
"get other.monthCode.toString",
"call other.monthCode.toString",
"get other.nanosecond",
"get other.nanosecond.valueOf",
"call other.nanosecond.valueOf",
"get other.offset",
"get other.offset.toString",
"call other.offset.toString",
"get other.second",
"get other.second.valueOf",
"call other.second.valueOf",
"get other.timeZone",
"get other.year",
"get other.year.valueOf",
"call other.year.valueOf",
"has other.timeZone.getOffsetNanosecondsFor",
"has other.timeZone.getPossibleInstantsFor",
"has other.timeZone.id",
"get other.calendar.dateFromFields",
"call other.calendar.dateFromFields",
"get other.timeZone.getPossibleInstantsFor",
"call other.timeZone.getPossibleInstantsFor",
"get other.timeZone.getOffsetNanosecondsFor",
"call other.timeZone.getOffsetNanosecondsFor",
// NOTE: extra because of wall-clock time ambiguity:
"get other.timeZone.getOffsetNanosecondsFor",
"call other.timeZone.getOffsetNanosecondsFor",
// CalendarEquals
"get this.calendar.id",
"get other.calendar.id",
// CopyDataProperties
"ownKeys options",
"getOwnPropertyDescriptor options.roundingIncrement",
"get options.roundingIncrement",
"getOwnPropertyDescriptor options.roundingMode",
"get options.roundingMode",
"getOwnPropertyDescriptor options.largestUnit",
"get options.largestUnit",
"getOwnPropertyDescriptor options.smallestUnit",
"get options.smallestUnit",
"getOwnPropertyDescriptor options.additional",
"get options.additional",
// GetDifferenceSettings
"get options.largestUnit.toString",
"call options.largestUnit.toString",
"get options.roundingIncrement.valueOf",
"call options.roundingIncrement.valueOf",
"get options.roundingMode.toString",
"call options.roundingMode.toString",
"get options.smallestUnit.toString",
"call options.smallestUnit.toString",
// TimeZoneEquals
"get this.timeZone.id",
"get other.timeZone.id",
// DifferenceZonedDateTime
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
], "order of operations with identical wall-clock times and largestUnit a calendar unit");
actual.splice(0); // clear
// Making largestUnit a calendar unit adds the following observable operations:
const expectedOpsForCalendarDifference = [
// TimeZoneEquals
@ -192,9 +337,6 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays → DifferenceISODateTime
"get this.calendar.dateUntil",
"call this.calendar.dateUntil",
// NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
@ -218,9 +360,6 @@ const expectedOpsForCalendarRounding = [
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// RoundDuration → NanosecondsToDays → DifferenceISODateTime
"get this.calendar.dateUntil",
"call this.calendar.dateUntil",
// RoundDuration → NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",

View File

@ -14,4 +14,4 @@ const calendar = TemporalHelpers.calendarCheckOptionsPrototypePollution();
const instance = new Temporal.ZonedDateTime(0n, "UTC", calendar);
const argument = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, "UTC", calendar);
instance.until(argument, { largestUnit: "year" });
assert.sameValue(calendar.dateUntilCallCount, 2, "dateUntil should have been called on the calendar");
assert.sameValue(calendar.dateUntilCallCount, 1, "dateUntil should have been called on the calendar");

View File

@ -55,10 +55,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
earlier.until(later, { largestUnit });
},
{
years: ["year", "day"],
months: ["month", "day"],
weeks: ["week", "day"],
days: ["day", "day"],
years: ["year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],
@ -72,15 +72,15 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
(calendar, largestUnit) => {
const earlier = new Temporal.ZonedDateTime(0n, "UTC", calendar);
const earlier = new Temporal.ZonedDateTime(-31536000_000_000_000n /* = -365 days */, "UTC", calendar);
const later = new Temporal.ZonedDateTime(86_399_999_999_999n, "UTC", calendar);
earlier.until(later, { largestUnit, roundingIncrement: 2, roundingMode: 'ceil' });
},
{
years: ["year", "day", "day", "day"],
months: ["month", "day", "day", "day"],
weeks: ["week", "day", "day", "day"],
days: ["day", "day", "day", "day"],
years: ["year", "year"],
months: ["month", "month"],
weeks: ["week", "week"],
days: [],
hours: [],
minutes: [],
seconds: [],
@ -100,10 +100,10 @@ TemporalHelpers.checkCalendarDateUntilLargestUnitSingular(
earlier.until(later, { smallestUnit });
},
{
years: ["year", "day", "day", "year"],
months: ["month", "day", "day"],
weeks: ["week", "day", "day"],
days: ["day", "day", "day"],
years: ["year", "year"],
months: ["month"],
weeks: ["week"],
days: [],
hours: [],
minutes: [],
seconds: [],

View File

@ -1,59 +0,0 @@
// Copyright (C) 2022 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: >
NanosecondsToDays can loop arbitrarily long, performing observable operations each iteration.
info: |
NanosecondsToDays ( nanoseconds, relativeTo )
...
15. If sign is 1, then
a. Repeat, while days > 0 and intermediateNs > endNs,
i. Set days to days - 1.
ii. Set intermediateNs to (? AddZonedDateTime((startNs), relativeTo.[[TimeZone]],
relativeTo.[[Calendar]], 0, 0, 0, days, 0, 0, 0, 0, 0, 0)).
...
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const calls = [];
const dayLengthNs = 86400000000000n;
const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
function createRelativeTo(count) {
const tz = new Temporal.TimeZone("UTC");
// Record calls in calls[]
TemporalHelpers.observeMethod(calls, tz, "getPossibleInstantsFor");
const cal = new Temporal.Calendar("iso8601");
// Return _count_ days for the second call to dateUntil, behaving normally after
TemporalHelpers.substituteMethod(cal, "dateUntil", [
TemporalHelpers.SUBSTITUTE_SKIP,
Temporal.Duration.from({ days: count }),
]);
return new Temporal.ZonedDateTime(0n, tz, cal);
}
let zdt = createRelativeTo(200);
calls.splice(0); // Reset calls list after ZonedDateTime construction
zdt.until(other, {
largestUnit: "day",
});
assert.sameValue(
calls.length,
200 + 1,
"Expected ZonedDateTime.until to call getPossibleInstantsFor correct number of times"
);
zdt = createRelativeTo(300);
calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction
zdt.until(other, {
largestUnit: "day",
});
assert.sameValue(
calls.length,
300 + 1,
"Expected ZonedDateTime.until to call getPossibleInstantsFor correct number of times"
);

View File

@ -108,6 +108,18 @@ const ownTimeZone = TemporalHelpers.timeZoneObserver(actual, "this.timeZone");
const ownCalendar = TemporalHelpers.calendarObserver(actual, "this.calendar");
const instance = new Temporal.ZonedDateTime(1_000_000_000_000_000_000n, ownTimeZone, ownCalendar);
const dstTimeZone = TemporalHelpers.springForwardFallBackTimeZone();
const ownDstTimeZone = TemporalHelpers.timeZoneObserver(actual, "this.timeZone", {
getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor,
getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor,
});
const otherDstTimeZone = TemporalHelpers.timeZoneObserver(actual, "other.timeZone", {
getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor,
getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor,
});
/* 2000-10-29T01:30-07:00, in the middle of the first repeated hour: */
const fallBackInstance = new Temporal.ZonedDateTime(972808200_000_000_000n, ownDstTimeZone, ownCalendar);
const otherDateTimePropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2004,
month: 5,
@ -169,6 +181,139 @@ assert.compareArray(actual, expected.concat([
]), "order of operations with identical dates and largestUnit a calendar unit");
actual.splice(0); // clear
// two ZonedDateTimes that denote the same wall-clock time in the time zone can
// avoid calling some calendar methods:
const fallBackPropertyBag = TemporalHelpers.propertyBagObserver(actual, {
year: 2000,
month: 10,
monthCode: "M10",
day: 29,
hour: 1,
minute: 30,
second: 0,
millisecond: 0,
microsecond: 0,
nanosecond: 0,
offset: "-08:00",
calendar: TemporalHelpers.calendarObserver(actual, "other.calendar"),
timeZone: otherDstTimeZone,
}, "other");
fallBackInstance.until(fallBackPropertyBag, createOptionsObserver({ largestUnit: "days" }));
assert.compareArray(actual, [
// ToTemporalZonedDateTime
"get other.calendar",
"has other.calendar.dateAdd",
"has other.calendar.dateFromFields",
"has other.calendar.dateUntil",
"has other.calendar.day",
"has other.calendar.dayOfWeek",
"has other.calendar.dayOfYear",
"has other.calendar.daysInMonth",
"has other.calendar.daysInWeek",
"has other.calendar.daysInYear",
"has other.calendar.fields",
"has other.calendar.id",
"has other.calendar.inLeapYear",
"has other.calendar.mergeFields",
"has other.calendar.month",
"has other.calendar.monthCode",
"has other.calendar.monthDayFromFields",
"has other.calendar.monthsInYear",
"has other.calendar.weekOfYear",
"has other.calendar.year",
"has other.calendar.yearMonthFromFields",
"has other.calendar.yearOfWeek",
"get other.calendar.fields",
"call other.calendar.fields",
"get other.day",
"get other.day.valueOf",
"call other.day.valueOf",
"get other.hour",
"get other.hour.valueOf",
"call other.hour.valueOf",
"get other.microsecond",
"get other.microsecond.valueOf",
"call other.microsecond.valueOf",
"get other.millisecond",
"get other.millisecond.valueOf",
"call other.millisecond.valueOf",
"get other.minute",
"get other.minute.valueOf",
"call other.minute.valueOf",
"get other.month",
"get other.month.valueOf",
"call other.month.valueOf",
"get other.monthCode",
"get other.monthCode.toString",
"call other.monthCode.toString",
"get other.nanosecond",
"get other.nanosecond.valueOf",
"call other.nanosecond.valueOf",
"get other.offset",
"get other.offset.toString",
"call other.offset.toString",
"get other.second",
"get other.second.valueOf",
"call other.second.valueOf",
"get other.timeZone",
"get other.year",
"get other.year.valueOf",
"call other.year.valueOf",
"has other.timeZone.getOffsetNanosecondsFor",
"has other.timeZone.getPossibleInstantsFor",
"has other.timeZone.id",
"get other.calendar.dateFromFields",
"call other.calendar.dateFromFields",
"get other.timeZone.getPossibleInstantsFor",
"call other.timeZone.getPossibleInstantsFor",
"get other.timeZone.getOffsetNanosecondsFor",
"call other.timeZone.getOffsetNanosecondsFor",
// NOTE: extra because of wall-clock time ambiguity:
"get other.timeZone.getOffsetNanosecondsFor",
"call other.timeZone.getOffsetNanosecondsFor",
// CalendarEquals
"get this.calendar.id",
"get other.calendar.id",
// CopyDataProperties
"ownKeys options",
"getOwnPropertyDescriptor options.roundingIncrement",
"get options.roundingIncrement",
"getOwnPropertyDescriptor options.roundingMode",
"get options.roundingMode",
"getOwnPropertyDescriptor options.largestUnit",
"get options.largestUnit",
"getOwnPropertyDescriptor options.smallestUnit",
"get options.smallestUnit",
"getOwnPropertyDescriptor options.additional",
"get options.additional",
// GetDifferenceSettings
"get options.largestUnit.toString",
"call options.largestUnit.toString",
"get options.roundingIncrement.valueOf",
"call options.roundingIncrement.valueOf",
"get options.roundingMode.toString",
"call options.roundingMode.toString",
"get options.smallestUnit.toString",
"call options.smallestUnit.toString",
// TimeZoneEquals
"get this.timeZone.id",
"get other.timeZone.id",
// DifferenceZonedDateTime
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
], "order of operations with identical wall-clock times and largestUnit a calendar unit");
actual.splice(0); // clear
// Making largestUnit a calendar unit adds the following observable operations:
const expectedOpsForCalendarDifference = [
// TimeZoneEquals
@ -192,9 +337,6 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// NanosecondsToDays → DifferenceISODateTime
"get this.calendar.dateUntil",
"call this.calendar.dateUntil",
// NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",
@ -218,9 +360,6 @@ const expectedOpsForCalendarRounding = [
"call this.timeZone.getOffsetNanosecondsFor",
"get this.timeZone.getOffsetNanosecondsFor",
"call this.timeZone.getOffsetNanosecondsFor",
// RoundDuration → NanosecondsToDays → DifferenceISODateTime
"get this.calendar.dateUntil",
"call this.calendar.dateUntil",
// RoundDuration → NanosecondsToDays → AddDaysToZonedDateTime
"get this.timeZone.getPossibleInstantsFor",
"call this.timeZone.getPossibleInstantsFor",