Temporal: Tests for correct intermediate value in ZonedDateTime difference/rounding

These test cases ensure that DST disambiguation does not take place on
intermediate values that are not the start or the end of the calculation.

Note that NormalizedTimeDurationToDays is no longer called inside
Temporal.Duration.prototype.add/subtract, so a few tests can be deleted.

Other tests need to be adjusted because NormalizedTimeDurationToDays is
no longer called inside Temporal.ZonedDateTime.prototype.since/until via
DifferenceZonedDateTime, although it is still called as part of rounding.

In addition, new tests for the now-fixed edge case are added, and for the
day corrections that can happen to intermediates.

See https://github.com/tc39/proposal-temporal/pull/2760
This commit is contained in:
Philip Chimento 2024-01-19 14:28:16 -08:00 committed by Philip Chimento
parent d19cb1557c
commit 984f3cc284
26 changed files with 592 additions and 482 deletions

View File

@ -14,4 +14,4 @@ const calendar = TemporalHelpers.calendarDateAddUndefinedOptions();
const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9);
const instance = new Temporal.Duration(1, 1, 1, 1);
instance.add(instance, { relativeTo: new Temporal.ZonedDateTime(0n, timeZone, calendar) });
assert.sameValue(calendar.dateAddCallCount, 3);
assert.sameValue(calendar.dateAddCallCount, 2);

View File

@ -0,0 +1,52 @@
// 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.duration.prototype.add
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 duration1 = new Temporal.Duration(0, 0, /* weeks = */ 7, 0, /* hours = */ 12);
const duration2 = new Temporal.Duration(0, 0, 0, /* days = */ 1);
{
const tz = new (class extends Temporal.TimeZone {
getPossibleInstantsFor(dateTime) {
return super.getPossibleInstantsFor(dateTime.add({ days: 3 }));
}
})("UTC");
const relativeTo = new Temporal.ZonedDateTime(0n, tz);
assert.throws(RangeError, () => duration1.add(duration2, { relativeTo }),
"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 relativeTo = new Temporal.ZonedDateTime(0n, "UTC", cal);
assert.throws(RangeError, () => duration1.add(duration2, { relativeTo }),
"Calendar calculation causing mixed-sign values should cause RangeError");
}

View File

@ -1,46 +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: >
NormalizedTimeDurationToDays should not be able to loop arbitrarily.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
...
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 duration = Temporal.Duration.from({ days: 1 });
const dayLengthNs = 86400000000000n;
const dayInstant = new Temporal.Instant(dayLengthNs);
let calls = 0;
const timeZone = new class extends Temporal.TimeZone {
getPossibleInstantsFor() {
calls++;
return [dayInstant];
}
}("UTC");
const relativeTo = new Temporal.ZonedDateTime(0n, timeZone);
assert.throws(RangeError, () => duration.add(duration, { relativeTo }), "arbitrarily long loop is prevented");
assert.sameValue(calls, 5, "getPossibleInstantsFor is not called in an arbitrarily long loop");
// Expected calls:
// AddDuration ->
// AddZonedDateTime (1)
// AddZonedDateTime (2)
// DifferenceZonedDateTime ->
// NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (3, step 12)
// AddDaysToZonedDateTime (4, step 15)
// AddDaysToZonedDateTime (5, step 18.d)

View File

@ -338,18 +338,8 @@ const expectedOpsForZonedRelativeTo = expected.concat([
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → DifferenceISODateTime
"call options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.calendar.dateUntil",
// AddDuration → DifferenceZonedDateTime → AddZonedDateTime
"call options.relativeTo.calendar.dateAdd",
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 1
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 2
"call options.relativeTo.timeZone.getPossibleInstantsFor",
]);
const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, {

View File

@ -1,143 +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: >
Abstract operation NormalizedTimeDurationToDays can throw four different
RangeErrors.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
23. If days < 0 and sign = 1, throw a RangeError exception.
24. If days > 0 and sign = -1, throw a RangeError exception.
...
26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
...
29. If dayLength 2⁵³, throw a RangeError exception.
features: [Temporal, BigInt]
includes: [temporalHelpers.js]
---*/
const dayNs = 86_400_000_000_000;
const dayDuration = Temporal.Duration.from({ days: 1 });
const epochInstant = new Temporal.Instant(0n);
function timeZoneSubstituteValues(
getPossibleInstantsFor,
getOffsetNanosecondsFor
) {
const tz = new Temporal.TimeZone("UTC");
TemporalHelpers.substituteMethod(
tz,
"getPossibleInstantsFor",
getPossibleInstantsFor
);
TemporalHelpers.substituteMethod(
tz,
"getOffsetNanosecondsFor",
getOffsetNanosecondsFor
);
return tz;
}
// Step 23: days < 0 and sign = 1
let zdt = new Temporal.ZonedDateTime(
-1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[epochInstant], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[epochInstant], // Returned in step 16, setting _relativeResult_
],
[
// Behave normally in 3 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
dayNs - 1, // Returned in step 8, setting _startDateTime_
-dayNs + 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Adding day to day sets largestUnit to 'day', avoids having any week/month/year components in differences
dayDuration.add(dayDuration, {
relativeTo: zdt,
}),
"days < 0 and sign = 1"
);
// Step 24: days > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[epochInstant], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[epochInstant], // Returned in step 16, setting _relativeResult_
],
[
// Behave normally in 3 calls made prior to NanosecondsToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
-dayNs + 1, // Returned in step 8, setting _startDateTime_
dayNs - 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Adding day to day sets largestUnit to 'day', avoids having any week/month/year components in differences
dayDuration.add(dayDuration, {
relativeTo: zdt,
}),
"days > 0 and sign = -1"
);
// Step 26: nanoseconds > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
0n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[new Temporal.Instant(-1n)], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_
[new Temporal.Instant(-4n)], // Returned in step 19, setting _oneDayFarther_
],
[
// Behave normally in 3 calls made prior to NanosecondsToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
dayNs - 1, // Returned in step 8, setting _startDateTime_
-dayNs + 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Adding day to day sets largestUnit to 'day', avoids having any week/month/year components in differences
dayDuration.add(dayDuration, {
relativeTo: zdt,
}),
"nanoseconds > 0 and sign = -1"
);
// Step 29: day length is an unsafe integer
zdt = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 15
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 16
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for step 16, setting _relativeResult_
// Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_
[new Temporal.Instant(2n ** 53n + 2n * BigInt(dayNs))],
],
[]
)
);
assert.throws(RangeError, () =>
dayDuration.add(dayDuration, {
relativeTo: zdt,
}),
"Should throw RangeError when time zone calculates an outrageous day length"
);

View File

@ -0,0 +1,45 @@
// 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.duration.prototype.round
description: >
Rounding the resulting duration takes the time zone's UTC offset shifts
into account
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
// Based on a test case by Adam Shaw
{
// Date part of duration lands on skipped DST hour, causing disambiguation
const duration = new Temporal.Duration(0, 1, 0, 15, 12);
const relativeTo = new Temporal.ZonedDateTime(
950868000_000_000_000n /* = 2000-02-18T10Z */,
timeZone); /* = 2000-02-18T02-08 in local time */
TemporalHelpers.assertDuration(duration.round({ smallestUnit: "months", relativeTo }),
0, 2, 0, 0, 0, 0, 0, 0, 0, 0,
"1 month 15 days 12 hours should be exactly 1.5 months, which rounds up to 2 months");
TemporalHelpers.assertDuration(duration.round({ smallestUnit: "months", roundingMode: 'halfTrunc', relativeTo }),
0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
"1 month 15 days 12 hours should be exactly 1.5 months, which rounds down to 1 month");
}
{
// Month-only part of duration lands on skipped DST hour, should not cause
// disambiguation
const duration = new Temporal.Duration(0, 1, 0, 15);
const relativeTo = new Temporal.ZonedDateTime(
951991200_000_000_000n /* = 2000-03-02T10Z */,
timeZone); /* = 2000-03-02T02-08 in local time */
TemporalHelpers.assertDuration(duration.round({ smallestUnit: "months", relativeTo }),
0, 2, 0, 0, 0, 0, 0, 0, 0, 0,
"1 month 15 days should be exactly 1.5 months, which rounds up to 2 months");
TemporalHelpers.assertDuration(duration.round({ smallestUnit: "months", roundingMode: 'halfTrunc', relativeTo }),
0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
"1 month 15 days should be exactly 1.5 months, which rounds down to 1 month");
}

View File

@ -14,4 +14,4 @@ const calendar = TemporalHelpers.calendarDateAddUndefinedOptions();
const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9);
const instance = new Temporal.Duration(1, 1, 1, 1);
instance.subtract(new Temporal.Duration(-1, -1, -1, -1), { relativeTo: new Temporal.ZonedDateTime(0n, timeZone, calendar) });
assert.sameValue(calendar.dateAddCallCount, 3);
assert.sameValue(calendar.dateAddCallCount, 2);

View File

@ -0,0 +1,52 @@
// 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.duration.prototype.subtract
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 duration1 = new Temporal.Duration(0, 0, /* weeks = */ 7, 0, /* hours = */ 12);
const duration2 = new Temporal.Duration(0, 0, 0, /* days = */ -1);
{
const tz = new (class extends Temporal.TimeZone {
getPossibleInstantsFor(dateTime) {
return super.getPossibleInstantsFor(dateTime.add({ days: 3 }));
}
})("UTC");
const relativeTo = new Temporal.ZonedDateTime(0n, tz);
assert.throws(RangeError, () => duration1.subtract(duration2, { relativeTo }),
"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 relativeTo = new Temporal.ZonedDateTime(0n, "UTC", cal);
assert.throws(RangeError, () => duration1.subtract(duration2, { relativeTo }),
"Calendar calculation causing mixed-sign values should cause RangeError");
}

View File

@ -1,46 +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: >
NormalizedTimeDurationToDays should not be able to loop arbitrarily.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] )
...
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 duration = Temporal.Duration.from({ days: 1 });
const dayLengthNs = 86400000000000n;
const dayInstant = new Temporal.Instant(dayLengthNs);
let calls = 0;
const timeZone = new class extends Temporal.TimeZone {
getPossibleInstantsFor() {
calls++;
return [dayInstant];
}
}("UTC");
const relativeTo = new Temporal.ZonedDateTime(0n, timeZone);
assert.throws(RangeError, () => duration.subtract(duration, { relativeTo }), "indefinite loop is prevented");
assert.sameValue(calls, 5, "getPossibleInstantsFor is not called indefinitely");
// Expected calls:
// AddDuration ->
// AddZonedDateTime (1)
// AddZonedDateTime (2)
// DifferenceZonedDateTime ->
// NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (3, step 12)
// AddDaysToZonedDateTime (4, step 15)
// AddDaysToZonedDateTime (5, step 18.d)

View File

@ -338,18 +338,8 @@ const expectedOpsForZonedRelativeTo = expected.concat([
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → DifferenceISODateTime
"call options.relativeTo.timeZone.getPossibleInstantsFor",
"call options.relativeTo.calendar.dateUntil",
// AddDuration → DifferenceZonedDateTime → AddZonedDateTime
"call options.relativeTo.calendar.dateAdd",
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
"call options.relativeTo.timeZone.getOffsetNanosecondsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 1
"call options.relativeTo.timeZone.getPossibleInstantsFor",
// AddDuration → DifferenceZonedDateTime → NanosecondsToDays → AddZonedDateTime 2
"call options.relativeTo.timeZone.getPossibleInstantsFor",
]);
const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, {

View File

@ -1,141 +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: >
Abstract operation NormalizedTimeDurationToDays can throw four different
RangeErrors.
info: |
NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] )
23. If days < 0 and sign = 1, throw a RangeError exception.
24. If days > 0 and sign = -1, throw a RangeError exception.
...
26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception.
...
29. If dayLength 2⁵³, throw a RangeError exception.
features: [Temporal, BigInt]
includes: [temporalHelpers.js]
---*/
const dayNs = 86_400_000_000_000;
const dayDuration = Temporal.Duration.from({ days: 1 });
const epochInstant = new Temporal.Instant(0n);
function timeZoneSubstituteValues(
getPossibleInstantsFor,
getOffsetNanosecondsFor
) {
const tz = new Temporal.TimeZone("UTC");
TemporalHelpers.substituteMethod(
tz,
"getPossibleInstantsFor",
getPossibleInstantsFor
);
TemporalHelpers.substituteMethod(
tz,
"getOffsetNanosecondsFor",
getOffsetNanosecondsFor
);
return tz;
}
// Step 23: days < 0 and sign = 1
let zdt = new Temporal.ZonedDateTime(
-1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[epochInstant], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[epochInstant], // Returned in step 16, setting _relativeResult_
],
[
// Behave normally in 3 calls made prior to NormalizedTimeDurationToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
dayNs - 1, // Returned in step 8, setting _startDateTime_
-dayNs + 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Subtracting day from day sets largestUnit to 'day', avoids having any week/month/year components in difference
dayDuration.subtract(dayDuration, {
relativeTo: zdt,
})
);
// Step 24: days > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
1n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[epochInstant], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[epochInstant], // Returned in step 16, setting _relativeResult_
],
[
// Behave normally in 3 calls made prior to NanosecondsToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
-dayNs + 1, // Returned in step 8, setting _startDateTime_
dayNs - 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Subtracting day from day sets largestUnit to 'day', avoids having any week/month/year components in difference
dayDuration.subtract(dayDuration, {
relativeTo: zdt,
})
);
// Step 26: nanoseconds > 0 and sign = -1
zdt = new Temporal.ZonedDateTime(
0n, // Set DifferenceZonedDateTime _ns1_
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for first call, AddDuration step 15
[new Temporal.Instant(-1n)], // Returned in AddDuration step 16, setting _endNs_ -> DifferenceZonedDateTime _ns2_
[new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_
[new Temporal.Instant(-4n)], // Returned in step 19, setting _oneDayFarther_
],
[
// Behave normally in 3 calls made prior to NanosecondsToDays
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
TemporalHelpers.SUBSTITUTE_SKIP,
dayNs - 1, // Returned in step 8, setting _startDateTime_
-dayNs + 1, // Returned in step 9, setting _endDateTime_
]
)
);
assert.throws(RangeError, () =>
// Subtracting day from day sets largestUnit to 'day', avoids having any week/month/year components in difference
dayDuration.subtract(dayDuration, {
relativeTo: zdt,
})
);
// Step 29: day length is an unsafe integer
zdt = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
[
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 15
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for AddDuration step 16
TemporalHelpers.SUBSTITUTE_SKIP, // Behave normally for step 16, setting _relativeResult_
// Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_
[new Temporal.Instant(2n ** 53n - 3n * BigInt(dayNs))],
],
[]
)
);
const twoDaysDuration = new Temporal.Duration(0, 0, 0, 2);
assert.throws(RangeError, () =>
dayDuration.subtract(twoDaysDuration, {
relativeTo: zdt,
}),
"Should throw RangeError when time zone calculates an outrageous day length"
);

View File

@ -0,0 +1,37 @@
// 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.duration.prototype.total
description: >
Rounding the resulting duration takes the time zone's UTC offset shifts
into account
includes: [temporalHelpers.js]
features: [Temporal]
---*/
const timeZone = TemporalHelpers.springForwardFallBackTimeZone();
// Based on a test case by Adam Shaw
{
// Date part of duration lands on skipped DST hour, causing disambiguation
const duration = new Temporal.Duration(0, 1, 0, 15, 12);
const relativeTo = new Temporal.ZonedDateTime(
950868000_000_000_000n /* = 2000-02-18T10Z */,
timeZone); /* = 2000-02-18T02-08 in local time */
assert.sameValue(duration.total({ unit: "months", relativeTo }), 1.5,
"1 month 15 days 12 hours should be exactly 1.5 months");
}
{
// Month-only part of duration lands on skipped DST hour, should not cause
// disambiguation
const duration = new Temporal.Duration(0, 1, 0, 15);
const relativeTo = new Temporal.ZonedDateTime(
951991200_000_000_000n /* = 2000-03-02T10Z */,
timeZone); /* = 2000-03-02T02-08 in local time */
assert.sameValue(duration.total({ unit: "months", relativeTo }), 1.5,
"1 month 15 days should be exactly 1.5 months");
}

View File

@ -13,25 +13,15 @@ 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");

View File

@ -0,0 +1,53 @@
// 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");
}

View File

@ -0,0 +1,45 @@
// 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");
}

View File

@ -0,0 +1,73 @@
// 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);
{
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, "Normal case: no overflow, no DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
], "one intermediate should be tried");
}
calls.splice(0); // clear
{
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, "One day correction: overflow, no DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
], "two intermediates should be tried");
}
calls.splice(0); // clear
{
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, "One day correction: no overflow, 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
// Two days correction: overflow and DST
// (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, "Two days correction: overflow and DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // DisambiguatePossibleInstants on second intermediate
"call getPossibleInstantsFor", // third intermediate in DifferenceZonedDateTime
], "three intermediates should be tried, with disambiguation");
}

View File

@ -30,12 +30,14 @@ const timeZone = new class extends Temporal.TimeZone {
}("UTC");
const zdt = new Temporal.ZonedDateTime(0n, timeZone);
const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
const other = new Temporal.ZonedDateTime(dayLengthNs * 2n, "UTC", "iso8601");
assert.throws(RangeError, () => zdt.since(other, { largestUnit: "day" }), "indefinite loop is prevented");
assert.sameValue(calls, 3, "getPossibleInstantsFor is not called indefinitely");
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:
// DifferenceZonedDateTime -> NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (3, step 12)
// AddDaysToZonedDateTime (4, step 15)
// AddDaysToZonedDateTime (5, step 18.d)
// DifferenceTemporalZonedDateTime ->
// DifferenceZonedDateTime -> GetInstantFor (1)
// NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (2, step 12)
// AddDaysToZonedDateTime (3, step 15)
// AddDaysToZonedDateTime (4, step 18.d)

View File

@ -39,13 +39,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 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,
@ -66,7 +69,10 @@ assert.throws(RangeError, () =>
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,
@ -87,7 +93,10 @@ assert.throws(RangeError, () =>
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,
@ -108,9 +117,12 @@ assert.throws(RangeError, () =>
start = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
// 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)]],
[
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)],
],
[]
)
);

View File

@ -305,10 +305,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
@ -327,17 +323,8 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceISODateTime
"call this.timeZone.getPossibleInstantsFor",
"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",
];
const expectedOpsForCalendarRounding = [

View File

@ -13,25 +13,15 @@ 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");

View File

@ -0,0 +1,53 @@
// 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");
}

View File

@ -0,0 +1,45 @@
// 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");
}

View File

@ -0,0 +1,69 @@
// 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);
{
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, "Normal case: no overflow, no DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
], "one intermediate should be tried");
}
calls.splice(0); // clear
{
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, "One day correction: overflow, no DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
], "two intermediates should be tried");
}
calls.splice(0); // clear
{
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, "One day correction: no overflow, 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
{
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, "Two days correction: overflow and DST");
assert.compareArray(calls, [
"call getPossibleInstantsFor", // first intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // second intermediate in DifferenceZonedDateTime
"call getPossibleInstantsFor", // DisambiguatePossibleInstants on second intermediate
"call getPossibleInstantsFor", // third intermediate in DifferenceZonedDateTime
], "three intermediates should be tried, with disambiguation");
}

View File

@ -30,12 +30,14 @@ const timeZone = new class extends Temporal.TimeZone {
}("UTC");
const zdt = new Temporal.ZonedDateTime(0n, timeZone);
const other = new Temporal.ZonedDateTime(dayLengthNs, "UTC", "iso8601");
const other = new Temporal.ZonedDateTime(dayLengthNs * 2n, "UTC", "iso8601");
assert.throws(RangeError, () => zdt.until(other, { largestUnit: "day" }), "indefinite loop is prevented");
assert.sameValue(calls, 3, "getPossibleInstantsFor is not called indefinitely");
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:
// DifferenceZonedDateTime -> NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (3, step 12)
// AddDaysToZonedDateTime (4, step 15)
// AddDaysToZonedDateTime (5, step 18.d)
// DifferenceTemporalZonedDateTime ->
// DifferenceZonedDateTime -> GetInstantFor (1)
// NormalizedTimeDurationToDays ->
// AddDaysToZonedDateTime (2, step 12)
// AddDaysToZonedDateTime (3, step 15)
// AddDaysToZonedDateTime (4, step 18.d)

View File

@ -39,13 +39,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 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,
@ -66,7 +69,10 @@ assert.throws(RangeError, () =>
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,
@ -87,7 +93,10 @@ assert.throws(RangeError, () =>
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,
@ -108,9 +117,12 @@ assert.throws(RangeError, () =>
start = new Temporal.ZonedDateTime(
0n,
timeZoneSubstituteValues(
// 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)]],
[
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)],
],
[]
)
);

View File

@ -305,10 +305,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
@ -327,17 +323,8 @@ const expectedOpsForCalendarDifference = [
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceZonedDateTime
"call this.timeZone.getOffsetNanosecondsFor",
// DifferenceISODateTime
"call this.timeZone.getPossibleInstantsFor",
"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",
];
const expectedOpsForCalendarRounding = [