diff options
Diffstat (limited to '')
29 files changed, 758 insertions, 634 deletions
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/calendar-dateadd-called-with-options-undefined.js index 4955abacdd..84079298ce 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/calendar-dateadd-called-with-options-undefined.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/calendar-dateadd-called-with-options-undefined.js @@ -15,6 +15,6 @@ 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); reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/differencezoneddatetime-inconsistent-custom-calendar.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/differencezoneddatetime-inconsistent-custom-calendar.js new file mode 100644 index 0000000000..01bf6c756e --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/differencezoneddatetime-inconsistent-custom-calendar.js @@ -0,0 +1,55 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.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"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/normalized-time-duration-to-days-loop-arbitrarily.js deleted file mode 100644 index 62833e87c6..0000000000 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/normalized-time-duration-to-days-loop-arbitrarily.js +++ /dev/null @@ -1,78 +0,0 @@ -// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally -// 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 can loop arbitrarily up to max safe integer -info: | - NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] ) - ... - 21. Repeat, while done is false, - a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]], - relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign). - b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]], - relativeResult.[[EpochNanoseconds]]). - c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs). - c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then - i. Set norm to oneDayLess. - ii. Set relativeResult to oneDayFarther. - iii. Set days to days + sign. - d. Else, - i. Set done to true. -includes: [temporalHelpers.js] -features: [Temporal] ----*/ - -const calls = []; -const duration = Temporal.Duration.from({ days: 1 }); - -function createRelativeTo(count) { - const dayLengthNs = 86400000000000n; - const dayInstant = new Temporal.Instant(dayLengthNs); - const substitutions = []; - const timeZone = new Temporal.TimeZone("UTC"); - // Return constant value for first _count_ calls - TemporalHelpers.substituteMethod( - timeZone, - "getPossibleInstantsFor", - substitutions - ); - substitutions.length = count; - let i = 0; - for (i = 0; i < substitutions.length; i++) { - // (this value) - substitutions[i] = [dayInstant]; - } - // Record calls in calls[] - TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor"); - return new Temporal.ZonedDateTime(0n, timeZone); -} - -let zdt = createRelativeTo(50); -calls.splice(0); // Reset calls list after ZonedDateTime construction -duration.add(duration, { - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 50 + 1, - "Expected duration.add to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(100); -calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction -duration.add(duration, { - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 100 + 1, - "Expected duration.add to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(107); -assert.throws(RangeError, () => duration.add(duration, { relativeTo: zdt }), "107-2 days > 2⁵³ ns"); - -reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/order-of-operations.js index a530ba55e9..c3a2f2c4a9 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/order-of-operations.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/order-of-operations.js @@ -339,18 +339,8 @@ const expectedOpsForZonedRelativeTo = expected.concat([ "call options.relativeTo.timeZone.getPossibleInstantsFor", // AddDuration → DifferenceZonedDateTime "call options.relativeTo.timeZone.getOffsetNanosecondsFor", - // AddDuration → DifferenceZonedDateTime → DifferenceISODateTime - "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", + "call options.relativeTo.calendar.dateUntil", ]); const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, { diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-backward-offset-shift.js new file mode 100644 index 0000000000..a8506addba --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-backward-offset-shift.js @@ -0,0 +1,50 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.add +description: > + UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours. +features: [Temporal] +info: | + GetPossibleInstantsFor: + 5.b.i. Let _numResults_ be _list_'s length. + ii. If _numResults_ > 1, then + 1. Let _epochNs_ be a new empty List. + 2. For each value _instant_ in list, do + a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_. + 3. Let _min_ be the least element of the List _epochNs_. + 4. Let _max_ be the greatest element of the List _epochNs_. + 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + return 0; + } + + getPossibleInstantsFor(plainDateTime) { + const utc = new Temporal.TimeZone("UTC"); + const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime); + return [ + utcInstant.subtract({ hours: 12, nanoseconds: 1 }), + utcInstant.add({ hours: 12 }), + utcInstant, // add a third value in case the implementation doesn't sort + ]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 1); +assert.throws(RangeError, () => instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-forward-offset-shift.js new file mode 100644 index 0000000000..705327b70e --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-propertybag-out-of-range-forward-offset-shift.js @@ -0,0 +1,45 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.add +description: > + UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor + in DisambiguatePossibleInstants cannot be greater than 24 hours. +features: [Temporal] +info: | + DisambiguatePossibleInstants: + 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9; + return 12 * 3600e9 + 1; + } + + getPossibleInstantsFor(plainDateTime) { + const [utcInstant] = super.getPossibleInstantsFor(plainDateTime); + const { year, month, day } = plainDateTime; + + if (year < 1970) return [utcInstant.subtract({ hours: 12 })]; + if (year === 1970 && month === 1 && day === 1) return []; + return [utcInstant.add({ hours: 12, nanoseconds: 1 })]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 1); +assert.throws(RangeError, () => instance.add(new Temporal.Duration(0, 0, 0, 0, -24), { relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js deleted file mode 100644 index 7acd5929b6..0000000000 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/add/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js +++ /dev/null @@ -1,146 +0,0 @@ -// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally -// 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 ] ) - 22. If days < 0 and sign = 1, throw a RangeError exception. - 23. If days > 0 and sign = -1, throw a RangeError exception. - ... - 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. - ... - 28. 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 22: 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 23: 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 25: 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 21.a, 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 28: 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 21.a, 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" -); - -reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/dst-rounding-result.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/dst-rounding-result.js new file mode 100644 index 0000000000..daff4e93f6 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/dst-rounding-result.js @@ -0,0 +1,48 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.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"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/duration-out-of-range-added-to-relativeto.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/duration-out-of-range-added-to-relativeto.js index b564b27eb4..f63eb3b789 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/duration-out-of-range-added-to-relativeto.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/duration-out-of-range-added-to-relativeto.js @@ -8,21 +8,44 @@ description: RangeError thrown when calendar part of duration added to relativeT features: [Temporal] info: | RoundDuration: - 8.k. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). - l. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + 10.h. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + i. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + ... + 11.h. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + i. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + ... + 12.a. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + b. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + + UnbalanceDateDurationRelative: + 11. Let _yearsMonthsWeeksDuration_ be ! CreateTemporalDuration(_years_, _months_, _weeks_, 0, 0, 0, 0, 0, 0, 0). + 12. Let _later_ be ? CalendarDateAdd(_calendaRec_, _plainRelativeTo_, _yearsMonthsWeeksDuration_). + 13. Let _yearsMonthsWeeksInDays_ be DaysUntil(_plainRelativeTo_, _later_). + 14. Return ? CreateDateDurationRecord(0, 0, 0, _days_ + _yearsMonthsWeeksInDays_). ---*/ // Based on a test case by André Bargull <andre.bargull@gmail.com> -const instance = new Temporal.Duration(0, 0, 0, /* days = */ 500_000_000); const relativeTo = new Temporal.PlainDate(2000, 1, 1); -assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "years"})); -assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "months"})); -assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "weeks"})); - -const negInstance = new Temporal.Duration(0, 0, 0, /* days = */ -500_000_000); -assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "years"})); -assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "months"})); -assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "weeks"})); + +{ + const instance = new Temporal.Duration(0, 0, 0, /* days = */ 500_000_000); + assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "years"}), "days out of range, positive, smallestUnit years"); + assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "months"}), "days out of range, positive, smallestUnit months"); + assert.throws(RangeError, () => instance.round({relativeTo, smallestUnit: "weeks"}), "days out of range, positive, smallestUnit weeks"); + + const negInstance = new Temporal.Duration(0, 0, 0, /* days = */ -500_000_000); + assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "years"}), "days out of range, negative, smallestUnit years"); + assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "months"}), "days out of range, negative, smallestUnit months"); + assert.throws(RangeError, () => negInstance.round({relativeTo, smallestUnit: "weeks"}), "days out of range, negative, smallestUnit weeks"); +} + +{ + const instance = new Temporal.Duration(0, 0, /* weeks = */ 1, /* days = */ Math.trunc((2 ** 53) / 86_400)); + assert.throws(RangeError, () => instance.round({relativeTo, largestUnit: "days"}), "weeks + days out of range, positive"); + + const negInstance = new Temporal.Duration(0, 0, /* weeks = */ -1, /* days = */ -Math.trunc((2 ** 53) / 86_400)); + assert.throws(RangeError, () => instance.round({relativeTo, largestUnit: "days"}), "weeks + days out of range, negative"); +} reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/normalized-time-duration-to-days-loop-arbitrarily.js index 923d788246..ed63026c0a 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/normalized-time-duration-to-days-loop-arbitrarily.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/normalized-time-duration-to-days-loop-arbitrarily.js @@ -4,76 +4,44 @@ /*--- esid: sec-temporal.duration.prototype.round description: > - NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer + NormalizedTimeDurationToDays should not be able to loop arbitrarily. info: | NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] ) ... - 21. Repeat, while done is false, - a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]], - relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign). - b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]], - relativeResult.[[EpochNanoseconds]]). - c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs). - c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then - i. Set norm to oneDayLess. - ii. Set relativeResult to oneDayFarther. - iii. Set days to days + sign. - d. Else, - i. Set done to true. -includes: [temporalHelpers.js] + 22. If NormalizedTimeDurationSign(_oneDayLess_) × _sign_ ≥ 0, then + a. Set _norm_ to _oneDayLess_. + b. Set _relativeResult_ to _oneDayFarther_. + c. Set _days_ to _days_ + _sign_. + d. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_). + e. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], relativeResult.[[EpochNanoseconds]]). + f. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) × _sign_ ≥ 0, then + i. Throw a *RangeError* exception. features: [Temporal] ---*/ -const calls = []; const duration = Temporal.Duration.from({ days: 1 }); -function createRelativeTo(count) { - const dayLengthNs = 86400000000000n; - const dayInstant = new Temporal.Instant(dayLengthNs); - const substitutions = []; - const timeZone = new Temporal.TimeZone("UTC"); - // Return constant value for first _count_ calls - TemporalHelpers.substituteMethod( - timeZone, - "getPossibleInstantsFor", - substitutions - ); - substitutions.length = count; - let i = 0; - for (i = 0; i < substitutions.length; i++) { - // (this value) - substitutions[i] = [dayInstant]; +const dayLengthNs = 86400000000000n; +const dayInstant = new Temporal.Instant(dayLengthNs); +let calls = 0; +const timeZone = new class extends Temporal.TimeZone { + getPossibleInstantsFor() { + calls++; + return [dayInstant]; } - // Record calls in calls[] - TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor"); - return new Temporal.ZonedDateTime(0n, timeZone); -} +}("UTC"); -let zdt = createRelativeTo(50); -calls.splice(0); // Reset calls list after ZonedDateTime construction -duration.round({ - smallestUnit: "days", - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 50 + 1, - "Expected duration.round to call getPossibleInstantsFor correct number of times" -); +const relativeTo = new Temporal.ZonedDateTime(0n, timeZone); -zdt = createRelativeTo(100); -calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction -duration.round({ - smallestUnit: "days", - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 100 + 1, - "Expected duration.round to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(107); -assert.throws(RangeError, () => duration.round({ smallestUnit: "days", relativeTo: zdt }), "107-2 days > 2⁵³ ns"); +assert.throws(RangeError, () => duration.round({ smallestUnit: "days", relativeTo }), "indefinite loop is prevented"); +assert.sameValue(calls, 5, "getPossibleInstantsFor is not called indefinitely"); + // Expected calls: + // RoundDuration -> MoveRelativeZonedDateTime -> AddZonedDateTime (1) + // BalanceTimeDurationRelative -> + // AddZonedDateTime (2) + // NormalizedTimeDurationToDays -> + // AddDaysToZonedDateTime (3, step 12) + // AddDaysToZonedDateTime (4, step 15) + // AddDaysToZonedDateTime (5, step 18.d) reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-adjusting-rounded-days.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-adjusting-rounded-days.js new file mode 100644 index 0000000000..cd6c307e5b --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-adjusting-rounded-days.js @@ -0,0 +1,32 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.round +description: > + When adjusting the rounded days after rounding relative to a ZonedDateTime, + the duration may go out of range +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +// Based on a test case by André Bargull + +const d = new Temporal.Duration(0, 0, 0, /* days = */ 1, 0, 0, /* s = */ Number.MAX_SAFE_INTEGER - 86400, 0, 0, /* ns = */ 999_999_999); + +const timeZone = new Temporal.TimeZone("UTC"); +TemporalHelpers.substituteMethod(timeZone, 'getPossibleInstantsFor', [ + TemporalHelpers.SUBSTITUTE_SKIP, + [new Temporal.Instant(1n)], +]); + +const relativeTo = new Temporal.ZonedDateTime(0n, timeZone); + +assert.throws(RangeError, () => d.round({ + largestUnit: 'nanoseconds', + roundingIncrement: 2, + relativeTo +})); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-converting-from-normalized-duration.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-converting-from-normalized-duration.js index 4fde761351..d4f973d639 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-converting-from-normalized-duration.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/out-of-range-when-converting-from-normalized-duration.js @@ -14,6 +14,6 @@ const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, /* s = */ Number.MAX_SAFE_INTE assert.throws(RangeError, () => d.round({ largestUnit: "nanoseconds", roundingIncrement: 1, -}), "nanoseconds component is an unsafe integer after balancing"); +}), "nanoseconds component after balancing as a float64-representable integer is out of range"); reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-backward-offset-shift.js new file mode 100644 index 0000000000..e147558077 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-backward-offset-shift.js @@ -0,0 +1,50 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.round +description: > + UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours. +features: [Temporal] +info: | + GetPossibleInstantsFor: + 5.b.i. Let _numResults_ be _list_'s length. + ii. If _numResults_ > 1, then + 1. Let _epochNs_ be a new empty List. + 2. For each value _instant_ in list, do + a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_. + 3. Let _min_ be the least element of the List _epochNs_. + 4. Let _max_ be the greatest element of the List _epochNs_. + 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + return 0; + } + + getPossibleInstantsFor(plainDateTime) { + const utc = new Temporal.TimeZone("UTC"); + const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime); + return [ + utcInstant.subtract({ hours: 12, nanoseconds: 1 }), + utcInstant.add({ hours: 12 }), + utcInstant, // add a third value in case the implementation doesn't sort + ]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +assert.throws(RangeError, () => instance.round({ largestUnit: "years", relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-forward-offset-shift.js new file mode 100644 index 0000000000..f3b8d604ee --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-propertybag-out-of-range-forward-offset-shift.js @@ -0,0 +1,45 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.round +description: > + UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor + in DisambiguatePossibleInstants cannot be greater than 24 hours. +features: [Temporal] +info: | + DisambiguatePossibleInstants: + 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9; + return 12 * 3600e9 + 1; + } + + getPossibleInstantsFor(plainDateTime) { + const [utcInstant] = super.getPossibleInstantsFor(plainDateTime); + const { year, month, day } = plainDateTime; + + if (year < 1970) return [utcInstant.subtract({ hours: 12 })]; + if (year === 1970 && month === 1 && day === 1) return []; + return [utcInstant.add({ hours: 12, nanoseconds: 1 })]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +assert.throws(RangeError, () => instance.round({ largestUnit: "years", relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js index 150f9f419e..9c04b2991e 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/round/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js @@ -8,12 +8,12 @@ description: > RangeErrors. info: | NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) - 22. If days < 0 and sign = 1, throw a RangeError exception. - 23. If days > 0 and sign = -1, throw a RangeError exception. + 23. If days < 0 and sign = 1, throw a RangeError exception. + 24. If days > 0 and sign = -1, throw a RangeError exception. ... - 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. + 26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. ... - 28. If dayLength ≥ 2⁵³, throw a RangeError exception. + 29. If dayLength ≥ 2⁵³, throw a RangeError exception. features: [Temporal, BigInt] includes: [temporalHelpers.js] ---*/ @@ -41,7 +41,7 @@ function timeZoneSubstituteValues( return tz; } -// Step 22: days < 0 and sign = 1 +// Step 23: days < 0 and sign = 1 let zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( @@ -62,7 +62,7 @@ assert.throws(RangeError, () => "RangeError when days < 0 and sign = 1" ); -// Step 23: days > 0 and sign = -1 +// Step 24: days > 0 and sign = -1 zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( @@ -83,13 +83,13 @@ assert.throws(RangeError, () => "RangeError when days > 0 and sign = -1" ); -// Step 25: nanoseconds > 0 and sign = -1 +// Step 26: nanoseconds > 0 and sign = -1 zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( [ [new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_ - [new Temporal.Instant(-4n)], // Returned in step 21.a, setting _oneDayFarther_ + [new Temporal.Instant(-4n)], // Returned in step 19, setting _oneDayFarther_ ], [ TemporalHelpers.SUBSTITUTE_SKIP, // Pre-conversion in Duration.p.round @@ -107,12 +107,12 @@ assert.throws(RangeError, () => "RangeError when nanoseconds > 0 and sign = -1" ); -// Step 28: day length is an unsafe integer +// Step 29: day length is an unsafe integer zdt = new Temporal.ZonedDateTime( 0n, timeZoneSubstituteValues( // Not called in step 16 because _days_ = 0 - // Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_ + // Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_ [[new Temporal.Instant(2n ** 53n)]], [] ) diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/calendar-dateadd-called-with-options-undefined.js index 5f5155e292..e0c22a689e 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/calendar-dateadd-called-with-options-undefined.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/calendar-dateadd-called-with-options-undefined.js @@ -15,6 +15,6 @@ 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); reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/differencezoneddatetime-inconsistent-custom-calendar.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/differencezoneddatetime-inconsistent-custom-calendar.js new file mode 100644 index 0000000000..e2c543c7d2 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/differencezoneddatetime-inconsistent-custom-calendar.js @@ -0,0 +1,55 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.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"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/normalized-time-duration-to-days-loop-arbitrarily.js deleted file mode 100644 index 8873380037..0000000000 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/normalized-time-duration-to-days-loop-arbitrarily.js +++ /dev/null @@ -1,78 +0,0 @@ -// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally -// 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 can loop arbitrarily up to max safe integer -info: | - NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] ) - ... - 21. Repeat, while done is false, - a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]], - relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign). - b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]], - relativeResult.[[EpochNanoseconds]]). - c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs). - c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then - i. Set norm to oneDayLess. - ii. Set relativeResult to oneDayFarther. - iii. Set days to days + sign. - d. Else, - i. Set done to true. -includes: [temporalHelpers.js] -features: [Temporal] ----*/ - -const calls = []; -const duration = Temporal.Duration.from({ days: 1 }); - -function createRelativeTo(count) { - const dayLengthNs = 86400000000000n; - const dayInstant = new Temporal.Instant(dayLengthNs); - const substitutions = []; - const timeZone = new Temporal.TimeZone("UTC"); - // Return constant value for first _count_ calls - TemporalHelpers.substituteMethod( - timeZone, - "getPossibleInstantsFor", - substitutions - ); - substitutions.length = count; - let i = 0; - for (i = 0; i < substitutions.length; i++) { - // (this value) - substitutions[i] = [dayInstant]; - } - // Record calls in calls[] - TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor"); - return new Temporal.ZonedDateTime(0n, timeZone); -} - -let zdt = createRelativeTo(50); -calls.splice(0); // Reset calls list after ZonedDateTime construction -duration.subtract(duration, { - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 50 + 1, - "Expected duration.subtract to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(100); -calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction -duration.subtract(duration, { - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 100 + 1, - "Expected duration.subtract to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(107); -assert.throws(RangeError, () => duration.subtract(duration, { relativeTo: zdt }), "107-2 days > 2⁵³ ns"); - -reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/order-of-operations.js index 988835838f..55438a2b97 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/order-of-operations.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/order-of-operations.js @@ -339,18 +339,8 @@ const expectedOpsForZonedRelativeTo = expected.concat([ "call options.relativeTo.timeZone.getPossibleInstantsFor", // AddDuration → DifferenceZonedDateTime "call options.relativeTo.timeZone.getOffsetNanosecondsFor", - // AddDuration → DifferenceZonedDateTime → DifferenceISODateTime - "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", + "call options.relativeTo.calendar.dateUntil", ]); const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, { diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-backward-offset-shift.js new file mode 100644 index 0000000000..193c09e806 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-backward-offset-shift.js @@ -0,0 +1,50 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.subtract +description: > + UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours. +features: [Temporal] +info: | + GetPossibleInstantsFor: + 5.b.i. Let _numResults_ be _list_'s length. + ii. If _numResults_ > 1, then + 1. Let _epochNs_ be a new empty List. + 2. For each value _instant_ in list, do + a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_. + 3. Let _min_ be the least element of the List _epochNs_. + 4. Let _max_ be the greatest element of the List _epochNs_. + 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + return 0; + } + + getPossibleInstantsFor(plainDateTime) { + const utc = new Temporal.TimeZone("UTC"); + const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime); + return [ + utcInstant.subtract({ hours: 12, nanoseconds: 1 }), + utcInstant.add({ hours: 12 }), + utcInstant, // add a third value in case the implementation doesn't sort + ]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 1); +assert.throws(RangeError, () => instance.subtract(new Temporal.Duration(0, 0, 0, 0, 24), { relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-forward-offset-shift.js new file mode 100644 index 0000000000..3d1d6262f0 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-propertybag-out-of-range-forward-offset-shift.js @@ -0,0 +1,45 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.subtract +description: > + UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor + in DisambiguatePossibleInstants cannot be greater than 24 hours. +features: [Temporal] +info: | + DisambiguatePossibleInstants: + 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9; + return 12 * 3600e9 + 1; + } + + getPossibleInstantsFor(plainDateTime) { + const [utcInstant] = super.getPossibleInstantsFor(plainDateTime); + const { year, month, day } = plainDateTime; + + if (year < 1970) return [utcInstant.subtract({ hours: 12 })]; + if (year === 1970 && month === 1 && day === 1) return []; + return [utcInstant.add({ hours: 12, nanoseconds: 1 })]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 1); +assert.throws(RangeError, () => instance.subtract(new Temporal.Duration(0, 0, 0, 0, 24), { relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js deleted file mode 100644 index 46307bcaf9..0000000000 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/subtract/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js +++ /dev/null @@ -1,144 +0,0 @@ -// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally -// 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 ] ) - 22. If days < 0 and sign = 1, throw a RangeError exception. - 23. If days > 0 and sign = -1, throw a RangeError exception. - ... - 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. - ... - 28. 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 22: 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 23: 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 25: 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 21.a, 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 28: 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 21.a, 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" -); - -reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dst-rounding-result.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dst-rounding-result.js new file mode 100644 index 0000000000..b2b4daf2de --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dst-rounding-result.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.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"); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js index 2abd469541..cf33d39b0e 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js @@ -8,21 +8,44 @@ description: RangeError thrown when calendar part of duration added to relativeT features: [Temporal] info: | RoundDuration: - 8.k. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). - l. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + 10.h. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + i. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + ... + 11.h. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + i. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + ... + 12.a. Let _isoResult_ be ! AddISODate(_plainRelativeTo_.[[ISOYear]]. _plainRelativeTo_.[[ISOMonth]], _plainRelativeTo_.[[ISODay]], 0, 0, 0, truncate(_fractionalDays_), *"constrain"*). + b. Let _wholeDaysLater_ be ? CreateTemporalDate(_isoResult_.[[Year]], _isoResult_.[[Month]], _isoResult_.[[Day]], _calendar_). + + UnbalanceDateDurationRelative: + 11. Let _yearsMonthsWeeksDuration_ be ! CreateTemporalDuration(_years_, _months_, _weeks_, 0, 0, 0, 0, 0, 0, 0). + 12. Let _later_ be ? CalendarDateAdd(_calendaRec_, _plainRelativeTo_, _yearsMonthsWeeksDuration_). + 13. Let _yearsMonthsWeeksInDays_ be DaysUntil(_plainRelativeTo_, _later_). + 14. Return ? CreateDateDurationRecord(0, 0, 0, _days_ + _yearsMonthsWeeksInDays_). ---*/ // Based on a test case by André Bargull <andre.bargull@gmail.com> -const instance = new Temporal.Duration(0, 0, 0, /* days = */ 500_000_000); const relativeTo = new Temporal.PlainDate(2000, 1, 1); -assert.throws(RangeError, () => instance.total({relativeTo, unit: "years"})); -assert.throws(RangeError, () => instance.total({relativeTo, unit: "months"})); -assert.throws(RangeError, () => instance.total({relativeTo, unit: "weeks"})); - -const negInstance = new Temporal.Duration(0, 0, 0, /* days = */ -500_000_000); -assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "years"})); -assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "months"})); -assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "weeks"})); + +{ + const instance = new Temporal.Duration(0, 0, 0, /* days = */ 500_000_000); + assert.throws(RangeError, () => instance.total({relativeTo, unit: "years"}), "days out of range, positive, unit years"); + assert.throws(RangeError, () => instance.total({relativeTo, unit: "months"}), "days out of range, positive, unit months"); + assert.throws(RangeError, () => instance.total({relativeTo, unit: "weeks"}), "days out of range, positive, unit weeks"); + + const negInstance = new Temporal.Duration(0, 0, 0, /* days = */ -500_000_000); + assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "years"}), "days out of range, negative, unit years"); + assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "months"}), "days out of range, negative, unit months"); + assert.throws(RangeError, () => negInstance.total({relativeTo, unit: "weeks"}), "days out of range, negative, unit weeks"); +} + +{ + const instance = new Temporal.Duration(0, 0, /* weeks = */ 1, /* days = */ Math.trunc((2 ** 53) / 86_400)); + assert.throws(RangeError, () => instance.total({relativeTo, unit: "days"}), "weeks + days out of range, positive"); + + const negInstance = new Temporal.Duration(0, 0, /* weeks = */ -1, /* days = */ -Math.trunc((2 ** 53) / 86_400)); + assert.throws(RangeError, () => instance.total({relativeTo, unit: "days"}), "weeks + days out of range, negative"); +} reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js index 72260abd68..c4d685113d 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js @@ -5,76 +5,42 @@ /*--- esid: sec-temporal.duration.prototype.total description: > - NormalizedTimeDurationToDays can loop arbitrarily up to max safe integer + NormalizedTimeDurationToDays should not be able to loop arbitrarily. info: | NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDatetime ] ) ... - 21. Repeat, while done is false, - a. Let oneDayFarther be ? AddDaysToZonedDateTime(relativeResult.[[Instant]], - relativeResult.[[DateTime]], timeZoneRec, zonedRelativeTo.[[Calendar]], sign). - b. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(oneDayFarther.[[EpochNanoseconds]], - relativeResult.[[EpochNanoseconds]]). - c. Let oneDayLess be ? SubtractNormalizedTimeDuration(norm, dayLengthNs). - c. If NormalizedTimeDurationSign(oneDayLess) × sign ≥ 0, then - i. Set norm to oneDayLess. - ii. Set relativeResult to oneDayFarther. - iii. Set days to days + sign. - d. Else, - i. Set done to true. -includes: [temporalHelpers.js] + 22. If NormalizedTimeDurationSign(_oneDayLess_) × _sign_ ≥ 0, then + a. Set _norm_ to _oneDayLess_. + b. Set _relativeResult_ to _oneDayFarther_. + c. Set _days_ to _days_ + _sign_. + d. Set _oneDayFarther_ to ? AddDaysToZonedDateTime(_relativeResult_.[[Instant]], _relativeResult_.[[DateTime]], _timeZoneRec_, _zonedRelativeTo_.[[Calendar]], _sign_). + e. Set dayLengthNs to NormalizedTimeDurationFromEpochNanosecondsDifference(_oneDayFarther.[[EpochNanoseconds]], relativeResult.[[EpochNanoseconds]]). + f. If NormalizedTimeDurationSign(? SubtractNormalizedTimeDuration(_norm_, _dayLengthNs_)) × _sign_ ≥ 0, then + i. Throw a *RangeError* exception. features: [Temporal] ---*/ -const calls = []; const duration = Temporal.Duration.from({ days: 1 }); -function createRelativeTo(count) { - const dayLengthNs = 86400000000000n; - const dayInstant = new Temporal.Instant(dayLengthNs); - const substitutions = []; - const timeZone = new Temporal.TimeZone("UTC"); - // Return constant value for first _count_ calls - TemporalHelpers.substituteMethod( - timeZone, - "getPossibleInstantsFor", - substitutions - ); - substitutions.length = count; - let i = 0; - for (i = 0; i < substitutions.length; i++) { - // (this value) - substitutions[i] = [dayInstant]; +const dayLengthNs = 86400000000000n; +const dayInstant = new Temporal.Instant(dayLengthNs); +let calls = 0; +const timeZone = new class extends Temporal.TimeZone { + getPossibleInstantsFor() { + calls++; + return [dayInstant]; } - // Record calls in calls[] - TemporalHelpers.observeMethod(calls, timeZone, "getPossibleInstantsFor"); - return new Temporal.ZonedDateTime(0n, timeZone); -} +}("UTC"); -let zdt = createRelativeTo(50); -calls.splice(0); // Reset calls list after ZonedDateTime construction -duration.total({ - unit: "day", - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 50 + 2, - "Expected duration.total to call getPossibleInstantsFor correct number of times" -); +const relativeTo = new Temporal.ZonedDateTime(0n, timeZone); -zdt = createRelativeTo(100); -calls.splice(0); // Reset calls list after previous loop + ZonedDateTime construction -duration.total({ - unit: "day", - relativeTo: zdt, -}); -assert.sameValue( - calls.length, - 100 + 2, - "Expected duration.total to call getPossibleInstantsFor correct number of times" -); - -zdt = createRelativeTo(106); -assert.throws(RangeError, () => duration.total({ unit: "day", relativeTo: zdt }), "106-1 days > 2⁵³ ns"); +assert.throws(RangeError, () => duration.total({ unit: "days", relativeTo }), "indefinite loop is prevented"); +assert.sameValue(calls, 4, "getPossibleInstantsFor is not called indefinitely"); + // Expected calls: + // AddZonedDateTime (1) + // NormalizedTimeDurationToDays -> + // AddDaysToZonedDateTime (2, step 12) + // AddDaysToZonedDateTime (3, step 15) + // AddDaysToZonedDateTime (4, step 18.d) reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js index ab3fd772cc..9f00233078 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js @@ -71,7 +71,7 @@ function f64Repr(f) { const tz = new (class extends Temporal.TimeZone { getPossibleInstantsFor() { - // Called in NormalizedTimeDurationToDays 21.a from RoundDuration 7.b. + // Called in NormalizedTimeDurationToDays 19 from RoundDuration 7.b. // Sets _result_.[[DayLength]] to 2⁵³ - 1 ns, its largest possible value return [new Temporal.Instant(-86400_0000_0000_000_000_000n + 2n ** 53n - 1n)]; } diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-backward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-backward-offset-shift.js new file mode 100644 index 0000000000..83878fb1b2 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-backward-offset-shift.js @@ -0,0 +1,50 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + UTC offset shift returned by getPossibleInstantsFor can be at most 24 hours. +features: [Temporal] +info: | + GetPossibleInstantsFor: + 5.b.i. Let _numResults_ be _list_'s length. + ii. If _numResults_ > 1, then + 1. Let _epochNs_ be a new empty List. + 2. For each value _instant_ in list, do + a. Append _instant_.[[EpochNanoseconds]] to the end of the List _epochNs_. + 3. Let _min_ be the least element of the List _epochNs_. + 4. Let _max_ be the greatest element of the List _epochNs_. + 5. If abs(ℝ(_max_ - _min_)) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + return 0; + } + + getPossibleInstantsFor(plainDateTime) { + const utc = new Temporal.TimeZone("UTC"); + const [utcInstant] = utc.getPossibleInstantsFor(plainDateTime); + return [ + utcInstant.subtract({ hours: 12, nanoseconds: 1 }), + utcInstant.add({ hours: 12 }), + utcInstant, // add a third value in case the implementation doesn't sort + ]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-forward-offset-shift.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-forward-offset-shift.js new file mode 100644 index 0000000000..0012871d85 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-out-of-range-forward-offset-shift.js @@ -0,0 +1,45 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + UTC offset shift returned by adjacent invocations of getOffsetNanosecondsFor + in DisambiguatePossibleInstants cannot be greater than 24 hours. +features: [Temporal] +info: | + DisambiguatePossibleInstants: + 18. If abs(_nanoseconds_) > nsPerDay, throw a *RangeError* exception. +---*/ + +class ShiftLonger24Hour extends Temporal.TimeZone { + id = 'TestTimeZone'; + _shiftEpochNs = 12n * 3600n * 1_000_000_000n; // 1970-01-01T12:00Z + + constructor() { + super('UTC'); + } + + getOffsetNanosecondsFor(instant) { + if (instant.epochNanoseconds < this._shiftEpochNs) return -12 * 3600e9; + return 12 * 3600e9 + 1; + } + + getPossibleInstantsFor(plainDateTime) { + const [utcInstant] = super.getPossibleInstantsFor(plainDateTime); + const { year, month, day } = plainDateTime; + + if (year < 1970) return [utcInstant.subtract({ hours: 12 })]; + if (year === 1970 && month === 1 && day === 1) return []; + return [utcInstant.add({ hours: 12, nanoseconds: 1 })]; + } +} + +const timeZone = new ShiftLonger24Hour(); +const relativeTo = { year: 1970, month: 1, day: 1, hour: 12, timeZone }; + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "RangeError should be thrown"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js index d415117f89..3150c81852 100644 --- a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js @@ -8,12 +8,12 @@ description: > RangeErrors. info: | NormalizedTimeDurationToDays ( norm, zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) - 22. If days < 0 and sign = 1, throw a RangeError exception. - 23. If days > 0 and sign = -1, throw a RangeError exception. + 23. If days < 0 and sign = 1, throw a RangeError exception. + 24. If days > 0 and sign = -1, throw a RangeError exception. ... - 25. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. + 26. If NormalizedTimeDurationSign(_norm_) = 1 and sign = -1, throw a RangeError exception. ... - 28. If dayLength ≥ 2⁵³, throw a RangeError exception. + 29. If dayLength ≥ 2⁵³, throw a RangeError exception. features: [Temporal, BigInt] includes: [temporalHelpers.js] ---*/ @@ -41,7 +41,7 @@ function timeZoneSubstituteValues( return tz; } -// Step 22: days < 0 and sign = 1 +// Step 23: days < 0 and sign = 1 let zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( @@ -62,7 +62,7 @@ assert.throws(RangeError, () => "RangeError when days < 0 and sign = 1" ); -// Step 23: days > 0 and sign = -1 +// Step 24: days > 0 and sign = -1 zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( @@ -83,13 +83,13 @@ assert.throws(RangeError, () => "RangeError when days > 0 and sign = -1" ); -// Step 25: nanoseconds > 0 and sign = -1 +// Step 26: nanoseconds > 0 and sign = -1 zdt = new Temporal.ZonedDateTime( 0n, // Sets _startNs_ to 0 timeZoneSubstituteValues( [ [new Temporal.Instant(-2n)], // Returned in step 16, setting _relativeResult_ - [new Temporal.Instant(-4n)], // Returned in step 21.a, setting _oneDayFarther_ + [new Temporal.Instant(-4n)], // Returned in step 19, setting _oneDayFarther_ ], [ TemporalHelpers.SUBSTITUTE_SKIP, // pre-conversion in Duration.p.total @@ -107,12 +107,12 @@ assert.throws(RangeError, () => "RangeError when nanoseconds > 0 and sign = -1" ); -// Step 28: day length is an unsafe integer +// Step 29: day length is an unsafe integer zdt = new Temporal.ZonedDateTime( 0n, timeZoneSubstituteValues( // Not called in step 16 because _days_ = 0 - // Returned in step 21.a, making _oneDayFarther_ 2^53 ns later than _relativeResult_ + // Returned in step 19, making _oneDayFarther_ 2^53 ns later than _relativeResult_ [[new Temporal.Instant(2n ** 53n)]], [] ) |