mirror of https://github.com/tc39/test262.git
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:
parent
d19cb1557c
commit
984f3cc284
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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)
|
|
@ -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, {
|
||||
|
|
|
@ -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"
|
||||
);
|
|
@ -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");
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
}
|
|
@ -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)
|
|
@ -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, {
|
||||
|
|
|
@ -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"
|
||||
);
|
|
@ -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");
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
45
test/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js
vendored
Normal file
45
test/built-ins/Temporal/ZonedDateTime/prototype/since/dst-rounding-result.js
vendored
Normal 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");
|
||||
}
|
73
test/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js
vendored
Normal file
73
test/built-ins/Temporal/ZonedDateTime/prototype/since/find-intermediate-instant.js
vendored
Normal 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");
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)],
|
||||
],
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
45
test/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js
vendored
Normal file
45
test/built-ins/Temporal/ZonedDateTime/prototype/until/dst-rounding-result.js
vendored
Normal 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");
|
||||
}
|
69
test/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js
vendored
Normal file
69
test/built-ins/Temporal/ZonedDateTime/prototype/until/find-intermediate-instant.js
vendored
Normal 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");
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)],
|
||||
],
|
||||
[]
|
||||
)
|
||||
);
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in New Issue