diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /js/src/tests/test262/built-ins/Temporal/Duration/prototype/total | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/tests/test262/built-ins/Temporal/Duration/prototype/total')
81 files changed, 3517 insertions, 0 deletions
diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js new file mode 100644 index 0000000000..f4781725dd --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-negative-result.js @@ -0,0 +1,24 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: A negative duration result is balanced correctly by the modulo operation in NanosecondsToDays +info: | + sec-temporal-nanosecondstodays step 6: + 6. If Type(_relativeTo_) is not Object or _relativeTo_ does not have an [[InitializedTemporalZonedDateTime]] internal slot, then + a. Return the new Record { ..., [[Nanoseconds]]: abs(_nanoseconds_) modulo _dayLengthNs_ × _sign_, ... }. + sec-temporal-balanceduration step 4: + 4. If _largestUnit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then + a. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _relativeTo_). + sec-temporal.duration.prototype.round step 9: + 9. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], _unbalanceResult_.[[Hours]], _unbalanceResult_.[[Minutes]], _unbalanceResult_.[[Seconds]], _unbalanceResult_.[[Milliseconds]], _unbalanceResult_.[[Microseconds]], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_). +features: [Temporal] +---*/ + +const duration = new Temporal.Duration(0, 0, 0, 0, -60); +const result = duration.total({ unit: "days" }); +assert.sameValue(result, -2.5); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js new file mode 100644 index 0000000000..9e37051c5d --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/balance-subseconds.js @@ -0,0 +1,17 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: Balancing from subsecond units to seconds happens correctly +features: [Temporal] +---*/ + +const pos = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 999, 999999, 999999999); +assert.sameValue(pos.total("seconds"), 2.998998999); + +const neg = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, -999, -999999, -999999999); +assert.sameValue(neg.total("seconds"), -2.998998999); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js new file mode 100644 index 0000000000..0beb2f811a --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/branding.js @@ -0,0 +1,27 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Throw a TypeError if the receiver is invalid +features: [Symbol, Temporal] +---*/ + +const total = Temporal.Duration.prototype.total; + +assert.sameValue(typeof total, "function"); + +const args = ["hour"]; + +assert.throws(TypeError, () => total.apply(undefined, args), "undefined"); +assert.throws(TypeError, () => total.apply(null, args), "null"); +assert.throws(TypeError, () => total.apply(true, args), "true"); +assert.throws(TypeError, () => total.apply("", args), "empty string"); +assert.throws(TypeError, () => total.apply(Symbol(), args), "symbol"); +assert.throws(TypeError, () => total.apply(1, args), "1"); +assert.throws(TypeError, () => total.apply({}, args), "plain object"); +assert.throws(TypeError, () => total.apply(Temporal.Duration, args), "Temporal.Duration"); +assert.throws(TypeError, () => total.apply(Temporal.Duration.prototype, args), "Temporal.Duration.prototype"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/browser.js diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js new file mode 100644 index 0000000000..ca011bad96 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/builtin.js @@ -0,0 +1,36 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + Tests that Temporal.Duration.prototype.total + meets the requirements for built-in objects defined by the + introduction of chapter 17 of the ECMAScript Language Specification. +info: | + Built-in functions that are not constructors do not have a "prototype" property unless + otherwise specified in the description of a particular function. + + Unless specified otherwise, a built-in object that is callable as a function is a built-in + function object with the characteristics described in 10.3. Unless specified otherwise, the + [[Extensible]] internal slot of a built-in object initially has the value true. + + Unless otherwise specified every built-in function and every built-in constructor has the + Function prototype object [...] as the value of its [[Prototype]] internal slot. +features: [Temporal] +---*/ + +assert.sameValue(Object.isExtensible(Temporal.Duration.prototype.total), + true, "Built-in objects must be extensible."); + +assert.sameValue(Object.prototype.toString.call(Temporal.Duration.prototype.total), + "[object Function]", "Object.prototype.toString"); + +assert.sameValue(Object.getPrototypeOf(Temporal.Duration.prototype.total), + Function.prototype, "prototype"); + +assert.sameValue(Temporal.Duration.prototype.total.hasOwnProperty("prototype"), + false, "prototype property"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js new file mode 100644 index 0000000000..7185da66e6 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-options-undefined.js @@ -0,0 +1,43 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + BuiltinTimeZoneGetInstantFor calls Calendar.dateAdd with undefined as the + options value +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarDateAddUndefinedOptions(); +const timeZone = TemporalHelpers.oneShiftTimeZone(new Temporal.Instant(0n), 3600e9); +const relativeTo = new Temporal.ZonedDateTime(0n, timeZone, calendar); + +// Total of a calendar unit where larger calendar units have to be converted +// down, to cover the path that goes through UnbalanceDateDurationRelative +// The calls come from the path: +// Duration.total() -> UnbalanceDateDurationRelative -> calendar.dateAdd() + +const instance1 = new Temporal.Duration(1, 1, 1, 1, 1); +instance1.total({ unit: "days", relativeTo }); +assert.sameValue(calendar.dateAddCallCount, 1, "converting larger calendar units down"); + +// Total of a calendar unit where smaller calendar units have to be converted +// up, to cover the path that goes through MoveRelativeZonedDateTime +// The calls come from these paths: +// Duration.total() -> +// MoveRelativeZonedDateTime -> AddZonedDateTime -> BuiltinTimeZoneGetInstantFor -> calendar.dateAdd() +// BalanceDuration -> +// AddZonedDateTime -> BuiltinTimeZoneGetInstantFor -> calendar.dateAdd() +// RoundDuration -> +// MoveRelativeDate -> calendar.dateAdd() + +calendar.dateAddCallCount = 0; + +const instance2 = new Temporal.Duration(0, 0, 1, 1); +instance2.total({ unit: "weeks", relativeTo }); +assert.sameValue(calendar.dateAddCallCount, 3, "converting smaller calendar units up"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js new file mode 100644 index 0000000000..0afdda3671 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateadd-called-with-plaindate-instance.js @@ -0,0 +1,21 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + relativeTo parameters that are not ZonedDateTime or undefined, are always + converted to PlainDate for observable calendar calls +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarDateAddPlainDateInstance(); +const instance = new Temporal.Duration(1, 1, 1, 1); +const relativeTo = new Temporal.PlainDate(2000, 1, 1, calendar); +calendar.specificPlainDate = relativeTo; +instance.total({ unit: "days", relativeTo }); +assert(calendar.dateAddCallCount > 0, "assertions in calendar.dateAdd() should have been tested"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js new file mode 100644 index 0000000000..0485054ae0 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-dateuntil-called-with-singular-largestunit.js @@ -0,0 +1,68 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: The options object passed to calendar.dateUntil has a largestUnit property with its value in the singular form +info: | + sec-temporal.duration.prototype.total steps 7–11: + 7. Let _unbalanceResult_ be ? UnbalanceDateDurationRelative(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _unit_, _relativeTo_). + ... + 10. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], _unbalanceResult_.[[Hours]], _unbalanceResult_.[[Minutes]], _unbalanceResult_.[[Seconds]], _unbalanceResult_.[[Milliseconds]], _unbalanceResult_.[[Microseconds]], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_). + 11. Let _roundResult_ be ? RoundDuration(_unbalanceResult_.[[Years]], _unbalanceResult_.[[Months]], _unbalanceResult_.[[Weeks]], _balanceResult_.[[Days]], _balanceResult_.[[Hours]], _balanceResult_.[[Minutes]], _balanceResult_.[[Seconds]], _balanceResult_.[[Milliseconds]], _balanceResult_.[[Microseconds]], _balanceResult_.[[Nanoseconds]], 1, _unit_, *"trunc"*, _relativeTo_). + sec-temporal-unbalancedatedurationrelative step 3: + 3. If _largestUnit_ is *"month"*, then + ... + g. Let _untilOptions_ be ! OrdinaryObjectCreate(*null*). + h. Perform ! CreateDataPropertyOrThrow(_untilOptions_, *"largestUnit"*, *"month"*). + i. Let _untilResult_ be ? CalendarDateUntil(_calendarRec_.[[Receiver]], _plainRelativeTo_, _later_, _untilOptions_, _calendarRec_.[[DateUntil]]). + sec-temporal-balanceduration step 3.a: + 3. If _largestUnit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then + a. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _relativeTo_). + sec-temporal-roundduration steps 5.d and 8.n–p: + 5. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then + ... + d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_). + ... + 8. If _unit_ is *"year"*, then + ... + n. Let _untilOptions_ be ! OrdinaryObjectCreate(*null*). + o. Perform ! CreateDataPropertyOrThrow(_untilOptions_, *"largestUnit"*, *"year"*). + p. Let _timePassed_ be ? CalendarDateUntil(_calendar_, _relativeTo_, _daysLater_, _untilOptions_) + sec-temporal-nanosecondstodays step 11: + 11. 1. Let _dateDifference_ be ? DifferenceISODateTime(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]], _endDateTime_.[[ISOHour]], _endDateTime_.[[ISOMinute]], _endDateTime_.[[ISOSecond]], _endDateTime_.[[ISOMillisecond]], _endDateTime_.[[ISOMicrosecond]], _endDateTime_.[[ISONanosecond]], _relativeTo_.[[Calendar]], *"day"*). + sec-temporal-differenceisodatetime steps 9–11: + 9. Let _dateLargestUnit_ be ! LargerOfTwoTemporalUnits(*"day"*, _largestUnit_). + 10. Let _untilOptions_ be ? MergeLargestUnitOption(_options_, _dateLargestUnit_). + 11. Let _dateDifference_ be ? CalendarDateUntil(_calendar_, _date1_, _date2_, _untilOptions_). +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +// Check the paths that go through NanosecondsToDays: only one call in +// RoundDuration when the unit is a calendar unit. The others all +// have largestUnit: "day" so the difference is taken in ISO calendar space. + +const duration = new Temporal.Duration(0, 1, 1, 1, 1, 1, 1, 1, 1, 1); + +TemporalHelpers.checkCalendarDateUntilLargestUnitSingular( + (calendar, unit) => { + const relativeTo = new Temporal.ZonedDateTime(0n, "UTC", calendar); + duration.total({ unit, relativeTo }); + }, + { + years: ["year"], + months: ["month"], + weeks: ["week"], + days: [], + hours: [], + minutes: [], + seconds: [], + milliseconds: [], + microseconds: [], + nanoseconds: [] + } +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js new file mode 100644 index 0000000000..347d9a6a76 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-fields-iterable.js @@ -0,0 +1,34 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Verify the result of calendar.fields() is treated correctly. +info: | + sec-temporal.duration.prototype.total step 4: + 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_). + sec-temporal-torelativetemporalobject step 4.c: + c. Let _fieldNames_ be ? CalendarFields(_calendar_, « *"day"*, *"month"*, *"monthCode"*, *"year"* »). + sec-temporal-calendarfields step 4: + 4. Let _result_ be ? IterableToList(_fieldsArray_). +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const expected = [ + "day", + "month", + "monthCode", + "year", +]; + +const calendar = TemporalHelpers.calendarFieldsIterable(); +const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1); +duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar } }); + +assert.sameValue(calendar.fieldsCallCount, 1, "fields() method called once"); +assert.compareArray(calendar.fieldsCalledWith[0], expected, "fields() method called with correct args"); +assert(calendar.iteratorExhausted[0], "iterated through the whole iterable"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js new file mode 100644 index 0000000000..154980346c --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-possibly-required.js @@ -0,0 +1,49 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Calendar required when days = 0 but years/months/weeks non-zero +features: [Temporal] +---*/ + +const yearInstance = new Temporal.Duration(1999); +const monthInstance = new Temporal.Duration(0, 49); +const weekInstance = new Temporal.Duration(0, 0, 1); +const dayInstance = new Temporal.Duration(0, 0, 0, 42); + +let relativeTo = new Temporal.PlainDate(2021, 12, 15); + +assert.throws( + RangeError, + () => { yearInstance.total({ unit: "days" }); }, + "total a Duration with non-zero years fails without largest/smallest unit" +); +const yearResult = yearInstance.total({ unit: "days", relativeTo }); +assert.sameValue(yearResult, 730120, "year duration contains proper days"); + +assert.throws( + RangeError, + () => { monthInstance.total({ unit: "days" }); }, + "total a Duration with non-zero month fails without largest/smallest unit" +); + +const monthResult = monthInstance.total({ unit: "days", relativeTo }); +assert.sameValue(monthResult, 1492, "month duration contains proper days"); + +assert.throws( + RangeError, + () => { weekInstance.total({ unit: "days" }); }, + "total a Duration with non-zero weeks fails without largest/smallest unit" +); + +const weekResult = weekInstance.total({ unit: "days", relativeTo }); +assert.sameValue(weekResult, 7, "week duration contains proper days"); + +const dayResultWithoutRelative = dayInstance.total({ unit: "days" }); +const dayResultWithRelative = dayInstance.total({ unit: "days", relativeTo }); +assert.sameValue(dayResultWithoutRelative, 42, "day duration without relative-to part contains proper days"); +assert.sameValue(dayResultWithRelative, 42, "day duration with relative-to part contains proper days"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js new file mode 100644 index 0000000000..12b0b55148 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/calendar-temporal-object.js @@ -0,0 +1,29 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Fast path for converting other Temporal objects to Temporal.Calendar by reading internal slots +info: | + sec-temporal.duration.prototype.total step 4: + 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_). + sec-temporal-torelativetemporalobject step 4.b: + b. Let _calendar_ be ? GetTemporalCalendarWithISODefault(_item_). + sec-temporal-gettemporalcalendarwithisodefault step 2: + 2. Return ? ToTemporalCalendarWithISODefault(_calendar_). + sec-temporal-totemporalcalendarwithisodefault step 2: + 3. Return ? ToTemporalCalendar(_temporalCalendarLike_). + sec-temporal-totemporalcalendar step 1.a: + a. If _temporalCalendarLike_ has an [[InitializedTemporalDate]], [[InitializedTemporalDateTime]], [[InitializedTemporalMonthDay]], [[InitializedTemporalYearMonth]], or [[InitializedTemporalZonedDateTime]] internal slot, then + i. Return _temporalCalendarLike_.[[Calendar]]. +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +TemporalHelpers.checkToTemporalCalendarFastPath((temporalObject) => { + const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1); + duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar: temporalObject } }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js new file mode 100644 index 0000000000..4caf54a64c --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/constructor-in-calendar-fields.js @@ -0,0 +1,17 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.duration.prototype.total +description: If a calendar's fields() method returns a field named 'constructor', PrepareTemporalFields should throw a RangeError. +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarWithExtraFields(['constructor']); +const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' }; +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo })); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js new file mode 100644 index 0000000000..4ac47adda0 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/dateuntil-field.js @@ -0,0 +1,37 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + When consulting calendar.dateUntil() to calculate the number of months in a + year, the months property is not accessed on the result Duration +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const actual = []; + +class CalendarDateUntilObservable extends Temporal.Calendar { + dateUntil(...args) { + actual.push("call dateUntil"); + const returnValue = super.dateUntil(...args); + TemporalHelpers.observeProperty(actual, returnValue, "months", Infinity); + return returnValue; + } +} + +const calendar = new CalendarDateUntilObservable("iso8601"); +const relativeTo = new Temporal.PlainDate(2018, 10, 12, calendar); + +const expected = [ + "call dateUntil", +]; + +const years = new Temporal.Duration(2); +const result = years.total({ unit: "months", relativeTo }); +assert.sameValue(result, 24, "result"); +assert.compareArray(actual, expected, "operations"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js new file mode 100644 index 0000000000..516d4f56e8 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duplicate-calendar-fields.js @@ -0,0 +1,19 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.duration.prototype.total +description: If a calendar's fields() method returns duplicate field names, PrepareTemporalFields should throw a RangeError. +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +for (const extra_fields of [['foo', 'foo'], ['day'], ['month'], ['monthCode'], ['year']]) { + const calendar = TemporalHelpers.calendarWithExtraFields(extra_fields); + const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' }; + const instance = new Temporal.Duration(1, 0, 0, 0, 24); + + assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo })); +} + +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 new file mode 100644 index 0000000000..2abd469541 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/duration-out-of-range-added-to-relativeto.js @@ -0,0 +1,28 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: RangeError thrown when calendar part of duration added to relativeTo is out of range +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_). +---*/ + +// 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"})); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js new file mode 100644 index 0000000000..e805ba32bc --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/length.js @@ -0,0 +1,28 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Temporal.Duration.prototype.total.length is 1 +info: | + Every built-in function object, including constructors, has a "length" property whose value is + an integer. Unless otherwise specified, this value is equal to the largest number of named + arguments shown in the subclause headings for the function description. Optional parameters + (which are indicated with brackets: [ ]) or rest parameters (which are shown using the form + «...name») are not included in the default argument count. + + Unless otherwise specified, the "length" property of a built-in function object has the + attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }. +includes: [propertyHelper.js] +features: [Temporal] +---*/ + +verifyProperty(Temporal.Duration.prototype.total, "length", { + value: 1, + writable: false, + enumerable: false, + configurable: true, +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js new file mode 100644 index 0000000000..9356d71761 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/name.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Temporal.Duration.prototype.total.name is "total". +info: | + Every built-in function object, including constructors, that is not identified as an anonymous + function has a "name" property whose value is a String. Unless otherwise specified, this value + is the name that is given to the function in this specification. + + Unless otherwise specified, the "name" property of a built-in function object, if it exists, + has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }. +includes: [propertyHelper.js] +features: [Temporal] +---*/ + +verifyProperty(Temporal.Duration.prototype.total, "name", { + value: "total", + writable: false, + enumerable: false, + configurable: true, +}); + +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 new file mode 100644 index 0000000000..72260abd68 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/normalized-time-duration-to-days-loop-arbitrarily.js @@ -0,0 +1,80 @@ +// |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.total +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.total({ + unit: "day", + relativeTo: zdt, +}); +assert.sameValue( + calls.length, + 50 + 2, + "Expected duration.total to call getPossibleInstantsFor correct number of times" +); + +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"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js new file mode 100644 index 0000000000..193622f8c9 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/not-a-constructor.js @@ -0,0 +1,24 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + Temporal.Duration.prototype.total does not implement [[Construct]], is not new-able +info: | + Built-in function objects that are not identified as constructors do not implement the + [[Construct]] internal method unless otherwise specified in the description of a particular + function. +includes: [isConstructor.js] +features: [Reflect.construct, Temporal] +---*/ + +assert.throws(TypeError, () => { + new Temporal.Duration.prototype.total(); +}, "Calling as constructor"); + +assert.sameValue(isConstructor(Temporal.Duration.prototype.total), false, + "isConstructor(Temporal.Duration.prototype.total)"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js new file mode 100644 index 0000000000..3f0e77111a --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/options-wrong-type.js @@ -0,0 +1,27 @@ +// |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.total +description: TypeError thrown when options argument is missing or a non-string primitive +features: [BigInt, Symbol, Temporal] +---*/ + +const badOptions = [ + undefined, + null, + true, + Symbol(), + 1, + 2n, +]; + +const instance = new Temporal.Duration(0, 0, 0, 0, 1); +assert.throws(TypeError, () => instance.total(), "TypeError on missing options argument"); +for (const value of badOptions) { + assert.throws(TypeError, () => instance.total(value), + `TypeError on wrong options type ${typeof value}`); +}; + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js new file mode 100644 index 0000000000..24262e35f8 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/order-of-operations.js @@ -0,0 +1,396 @@ +// |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.total +description: Properties on objects passed to total() are accessed in the correct order +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const expected = [ + "get options.relativeTo", + "get options.unit", + "get options.unit.toString", + "call options.unit.toString", +]; +const actual = []; + +function createOptionsObserver({ unit = "nanoseconds", roundingMode = "halfExpand", roundingIncrement = 1, relativeTo = undefined } = {}) { + return TemporalHelpers.propertyBagObserver(actual, { + unit, + roundingMode, + roundingIncrement, + relativeTo, + }, "options"); +} + +const instance = new Temporal.Duration(0, 0, 0, 0, 2400); + +// basic order of observable operations, with no relativeTo +instance.total(createOptionsObserver({ unit: "nanoseconds" })); +assert.compareArray(actual, expected, "order of operations"); +actual.splice(0); // clear + +const expectedOpsForPlainRelativeTo = [ + // ToRelativeTemporalObject + "get options.relativeTo", + "get options.relativeTo.calendar", + "has options.relativeTo.calendar.dateAdd", + "has options.relativeTo.calendar.dateFromFields", + "has options.relativeTo.calendar.dateUntil", + "has options.relativeTo.calendar.day", + "has options.relativeTo.calendar.dayOfWeek", + "has options.relativeTo.calendar.dayOfYear", + "has options.relativeTo.calendar.daysInMonth", + "has options.relativeTo.calendar.daysInWeek", + "has options.relativeTo.calendar.daysInYear", + "has options.relativeTo.calendar.fields", + "has options.relativeTo.calendar.id", + "has options.relativeTo.calendar.inLeapYear", + "has options.relativeTo.calendar.mergeFields", + "has options.relativeTo.calendar.month", + "has options.relativeTo.calendar.monthCode", + "has options.relativeTo.calendar.monthDayFromFields", + "has options.relativeTo.calendar.monthsInYear", + "has options.relativeTo.calendar.weekOfYear", + "has options.relativeTo.calendar.year", + "has options.relativeTo.calendar.yearMonthFromFields", + "has options.relativeTo.calendar.yearOfWeek", + "get options.relativeTo.calendar.dateFromFields", + "get options.relativeTo.calendar.fields", + "call options.relativeTo.calendar.fields", + "get options.relativeTo.day", + "get options.relativeTo.day.valueOf", + "call options.relativeTo.day.valueOf", + "get options.relativeTo.hour", + "get options.relativeTo.microsecond", + "get options.relativeTo.millisecond", + "get options.relativeTo.minute", + "get options.relativeTo.month", + "get options.relativeTo.month.valueOf", + "call options.relativeTo.month.valueOf", + "get options.relativeTo.monthCode", + "get options.relativeTo.monthCode.toString", + "call options.relativeTo.monthCode.toString", + "get options.relativeTo.nanosecond", + "get options.relativeTo.offset", + "get options.relativeTo.second", + "get options.relativeTo.timeZone", + "get options.relativeTo.year", + "get options.relativeTo.year.valueOf", + "call options.relativeTo.year.valueOf", + "call options.relativeTo.calendar.dateFromFields", + // GetTemporalUnit + "get options.unit", + "get options.unit.toString", + "call options.unit.toString", + // lookup in Duration.p.total + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", +]; + +const plainRelativeTo = TemporalHelpers.propertyBagObserver(actual, { + year: 2001, + month: 5, + monthCode: "M05", + day: 2, + calendar: TemporalHelpers.calendarObserver(actual, "options.relativeTo.calendar"), +}, "options.relativeTo"); + +// basic order of observable operations, without rounding: +instance.total(createOptionsObserver({ unit: "nanoseconds", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForPlainRelativeTo, "order of operations for PlainDate relativeTo"); +actual.splice(0); // clear + +// code path through RoundDuration that rounds to the nearest year with minimal calendar calls: +const expectedOpsForMinimalYearRounding = expectedOpsForPlainRelativeTo.concat([ + // 12.d and 12.f not called because years, months, weeks are 0 + "call options.relativeTo.calendar.dateUntil", // 12.n + // 12.r not called because years, months, weeks are 0 + "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate +]); +instance.total(createOptionsObserver({ unit: "years", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForMinimalYearRounding, "order of operations with years = 0 and unit = years"); +actual.splice(0); // clear + +// code path through RoundDuration that rounds to the nearest year: +const expectedOpsForYearRounding = expectedOpsForPlainRelativeTo.concat([ + "call options.relativeTo.calendar.dateAdd", // 12.d + "call options.relativeTo.calendar.dateAdd", // 12.f + "call options.relativeTo.calendar.dateUntil", // 12.n + "call options.relativeTo.calendar.dateAdd", // 12.r MoveRelativeDate + "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate +]); +const instanceYears = new Temporal.Duration(1, 12, 0, 0, /* hours = */ 2400); +instanceYears.total(createOptionsObserver({ unit: "years", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForYearRounding, "order of operations with unit = years"); +actual.splice(0); // clear + +// code path through Duration.prototype.total that rounds to the nearest month: +const expectedOpsForMonthRounding = expectedOpsForPlainRelativeTo.concat([ + // UnbalanceDateDurationRelative + "call options.relativeTo.calendar.dateAdd", // 3.f + "call options.relativeTo.calendar.dateUntil", // 3.i + // RoundDuration + "call options.relativeTo.calendar.dateAdd", // 13.c + "call options.relativeTo.calendar.dateAdd", // 13.e + "call options.relativeTo.calendar.dateUntil", // 13.m + "call options.relativeTo.calendar.dateAdd", // 13.q MoveRelativeDate + "call options.relativeTo.calendar.dateAdd", // 13.w MoveRelativeDate +]); +const instance2 = new Temporal.Duration(1, 0, 0, 62); +instance2.total(createOptionsObserver({ unit: "months", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForMonthRounding, "order of operations with unit = months"); +actual.splice(0); // clear + +// code path through Duration.prototype.total that rounds to the nearest week: +const expectedOpsForWeekRounding = expectedOpsForPlainRelativeTo.concat([ + // UnbalanceDateDurationRelative + "call options.relativeTo.calendar.dateAdd", // 4.e + // RoundDuration + "call options.relativeTo.calendar.dateUntil", // 14.f + "call options.relativeTo.calendar.dateAdd", // 14.j MoveRelativeDate + "call options.relativeTo.calendar.dateAdd", // 14.p MoveRelativeDate +]); +const instance3 = new Temporal.Duration(1, 1, 0, 15); +instance3.total(createOptionsObserver({ unit: "weeks", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForWeekRounding, "order of operations with unit = weeks"); +actual.splice(0); // clear + +// code path through UnbalanceDateDurationRelative that rounds to the nearest day: +const expectedOpsForDayRounding = expectedOpsForPlainRelativeTo.concat([ + "call options.relativeTo.calendar.dateAdd", // 10 +]); +const instance4 = new Temporal.Duration(1, 1, 1) +instance4.total(createOptionsObserver({ unit: "days", relativeTo: plainRelativeTo })); +assert.compareArray(actual, expectedOpsForDayRounding, "order of operations with unit = days"); +actual.splice(0); // clear + +const expectedOpsForZonedRelativeTo = [ + // ToRelativeTemporalObject + "get options.relativeTo", + "get options.relativeTo.calendar", + "has options.relativeTo.calendar.dateAdd", + "has options.relativeTo.calendar.dateFromFields", + "has options.relativeTo.calendar.dateUntil", + "has options.relativeTo.calendar.day", + "has options.relativeTo.calendar.dayOfWeek", + "has options.relativeTo.calendar.dayOfYear", + "has options.relativeTo.calendar.daysInMonth", + "has options.relativeTo.calendar.daysInWeek", + "has options.relativeTo.calendar.daysInYear", + "has options.relativeTo.calendar.fields", + "has options.relativeTo.calendar.id", + "has options.relativeTo.calendar.inLeapYear", + "has options.relativeTo.calendar.mergeFields", + "has options.relativeTo.calendar.month", + "has options.relativeTo.calendar.monthCode", + "has options.relativeTo.calendar.monthDayFromFields", + "has options.relativeTo.calendar.monthsInYear", + "has options.relativeTo.calendar.weekOfYear", + "has options.relativeTo.calendar.year", + "has options.relativeTo.calendar.yearMonthFromFields", + "has options.relativeTo.calendar.yearOfWeek", + "get options.relativeTo.calendar.dateFromFields", + "get options.relativeTo.calendar.fields", + "call options.relativeTo.calendar.fields", + "get options.relativeTo.day", + "get options.relativeTo.day.valueOf", + "call options.relativeTo.day.valueOf", + "get options.relativeTo.hour", + "get options.relativeTo.hour.valueOf", + "call options.relativeTo.hour.valueOf", + "get options.relativeTo.microsecond", + "get options.relativeTo.microsecond.valueOf", + "call options.relativeTo.microsecond.valueOf", + "get options.relativeTo.millisecond", + "get options.relativeTo.millisecond.valueOf", + "call options.relativeTo.millisecond.valueOf", + "get options.relativeTo.minute", + "get options.relativeTo.minute.valueOf", + "call options.relativeTo.minute.valueOf", + "get options.relativeTo.month", + "get options.relativeTo.month.valueOf", + "call options.relativeTo.month.valueOf", + "get options.relativeTo.monthCode", + "get options.relativeTo.monthCode.toString", + "call options.relativeTo.monthCode.toString", + "get options.relativeTo.nanosecond", + "get options.relativeTo.nanosecond.valueOf", + "call options.relativeTo.nanosecond.valueOf", + "get options.relativeTo.offset", + "get options.relativeTo.offset.toString", + "call options.relativeTo.offset.toString", + "get options.relativeTo.second", + "get options.relativeTo.second.valueOf", + "call options.relativeTo.second.valueOf", + "get options.relativeTo.timeZone", + "get options.relativeTo.year", + "get options.relativeTo.year.valueOf", + "call options.relativeTo.year.valueOf", + "call options.relativeTo.calendar.dateFromFields", + "has options.relativeTo.timeZone.getOffsetNanosecondsFor", + "has options.relativeTo.timeZone.getPossibleInstantsFor", + "has options.relativeTo.timeZone.id", + "get options.relativeTo.timeZone.getOffsetNanosecondsFor", + "get options.relativeTo.timeZone.getPossibleInstantsFor", + // InterpretISODateTimeOffset + "call options.relativeTo.timeZone.getPossibleInstantsFor", + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", + // GetTemporalUnit + "get options.unit", + "get options.unit.toString", + "call options.unit.toString", +]; + +const zonedRelativeTo = TemporalHelpers.propertyBagObserver(actual, { + year: 2001, + month: 5, + monthCode: "M05", + day: 2, + hour: 6, + minute: 54, + second: 32, + millisecond: 987, + microsecond: 654, + nanosecond: 321, + offset: "+00:00", + calendar: TemporalHelpers.calendarObserver(actual, "options.relativeTo.calendar"), + timeZone: TemporalHelpers.timeZoneObserver(actual, "options.relativeTo.timeZone"), +}, "options.relativeTo"); + +// basic order of observable operations, without rounding: +instance.total(createOptionsObserver({ unit: "nanoseconds", relativeTo: zonedRelativeTo })); +assert.compareArray(actual, expectedOpsForZonedRelativeTo.concat([ + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", +]), "order of operations for ZonedDateTime relativeTo"); +actual.splice(0); // clear + +// code path through RoundDuration that rounds to the nearest year with minimal calendar operations: +const expectedOpsForMinimalYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([ + // ToTemporalDate + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", + // lookup in Duration.p.total + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", + // BalancePossiblyInfiniteDuration → NanosecondsToDays + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 7. GetPlainDateTimeFor + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 11. GetPlainDateTimeFor + // BalancePossiblyInfiniteDuration → NanosecondsToDays → AddDaysToZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // BalancePossiblyInfiniteDuration → NanosecondsToDays → AddDaysToZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", +], [ + // code path through RoundDuration that rounds to the nearest year: + // MoveRelativeZonedDateTime → AddDaysToZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // 12.d and 12.f not called because years, months, weeks are 0 + "call options.relativeTo.calendar.dateUntil", // 12.n + // 12.r not called because years, months, weeks are 0 + "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate +]); +instance.total(createOptionsObserver({ unit: "years", relativeTo: zonedRelativeTo })); +assert.compareArray( + actual, + expectedOpsForMinimalYearRoundingZoned, + "order of operations with years = 0, unit = years and ZonedDateTime relativeTo" +); +actual.splice(0); // clear + +// code path through RoundDuration that rounds to the nearest year: +const expectedOpsForYearRoundingZoned = expectedOpsForZonedRelativeTo.concat([ + // ToTemporalDate + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", + // lookup in Duration.p.total + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", + // MoveRelativeZonedDateTime → AddZonedDateTime + "call options.relativeTo.calendar.dateAdd", + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 8. GetPlainDateTimeFor + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 9. GetPlainDateTimeFor + // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays → AddDaysToZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // BalancePossiblyInfiniteTimeDurationRelative → NanosecondsToDays → AddDaysToZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime + "call options.relativeTo.calendar.dateAdd", + "call options.relativeTo.timeZone.getPossibleInstantsFor", + // RoundDuration + "call options.relativeTo.calendar.dateAdd", // 12.d + "call options.relativeTo.calendar.dateAdd", // 12.f + "call options.relativeTo.calendar.dateUntil", // 12.n + "call options.relativeTo.calendar.dateAdd", // 12.r MoveRelativeDate + "call options.relativeTo.calendar.dateAdd", // 12.x MoveRelativeDate +]); +instanceYears.total(createOptionsObserver({ unit: "years", relativeTo: zonedRelativeTo })); +assert.compareArray( + actual, + expectedOpsForYearRoundingZoned, + "order of operations with unit = years and ZonedDateTime relativeTo" +); +actual.splice(0); // clear + +// code path that hits UnbalanceDateDurationRelative and RoundDuration +const expectedOpsForUnbalanceRound = expectedOpsForZonedRelativeTo.concat([ + // ToTemporalDate + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", + // lookup in Duration.p.total + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", + // No user code calls in UnbalanceDateDurationRelative + // MoveRelativeZonedDateTime → AddZonedDateTime + "call options.relativeTo.calendar.dateAdd", + "call options.relativeTo.timeZone.getPossibleInstantsFor", // 13. GetInstantFor + // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime + "call options.relativeTo.calendar.dateAdd", + "call options.relativeTo.timeZone.getPossibleInstantsFor", // 13. GetInstantFor + // RoundDuration + "call options.relativeTo.calendar.dateAdd", // 13.c + "call options.relativeTo.calendar.dateAdd", // 13.e + "call options.relativeTo.calendar.dateUntil", // 13.m + "call options.relativeTo.calendar.dateAdd", // 13.w MoveRelativeDate +]); +new Temporal.Duration(0, 1, 1).total(createOptionsObserver({ unit: "months", relativeTo: zonedRelativeTo })); +assert.compareArray( + actual, + expectedOpsForUnbalanceRound, + "order of operations with unit = months and ZonedDateTime relativeTo" +); +actual.splice(0); // clear + +// code path that avoids converting Zoned twice in BalanceTimeDurationRelative +const expectedOpsForBalanceRound = expectedOpsForZonedRelativeTo.concat([ + // ToTemporalDate + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", + // lookup in Duration.p.total + "get options.relativeTo.calendar.dateAdd", + "get options.relativeTo.calendar.dateUntil", + // No user code calls in UnbalanceDateDurationRelative + // No user code calls in AddZonedDateTime (years, months, weeks = 0) + // BalanceTimeDurationRelative + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // 4.a + "call options.relativeTo.timeZone.getPossibleInstantsFor", // 4.b + "call options.relativeTo.timeZone.getOffsetNanosecondsFor", // NanosecondsToDays 9 + "call options.relativeTo.timeZone.getPossibleInstantsFor", // NanosecondsToDays 26 + "call options.relativeTo.timeZone.getPossibleInstantsFor", // NanosecondsToDays 31.a + // RoundDuration → MoveRelativeZonedDateTime → AddZonedDateTime + "call options.relativeTo.timeZone.getPossibleInstantsFor", // 10. GetInstantFor + // RoundDuration + "call options.relativeTo.calendar.dateUntil", // 14.f + "call options.relativeTo.calendar.dateAdd", // 14.j MoveRelativeDate + "call options.relativeTo.calendar.dateAdd", // 14.p MoveRelativeDate +]); +new Temporal.Duration(0, 0, 0, 1, 240).total(createOptionsObserver({ unit: "weeks", relativeTo: zonedRelativeTo })); +assert.compareArray( + actual, + expectedOpsForBalanceRound, + "order of operations with unit = weeks and no calendar units" +); +actual.splice(0); // clear + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js new file mode 100644 index 0000000000..9c74187855 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js @@ -0,0 +1,99 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + RoundDuration computes on exact mathematical values. +features: [Temporal] +---*/ + +// Return the next Number value in direction to +Infinity. +function nextUp(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? -1n : 1n); + return f64[0]; +} + +// Return the next Number value in direction to -Infinity. +function nextDown(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return -Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? 1n : -1n); + return f64[0]; +} + +let duration = Temporal.Duration.from({ + hours: 4000, + nanoseconds: 1, +}); + +let total = duration.total({unit: "hours"}); + +// From RoundDuration(): +// +// 7. Let fractionalSeconds be nanoseconds × 10^-9 + microseconds × 10^-6 + milliseconds × 10^-3 + seconds. +// = nanoseconds × 10^-9 +// = 1 × 10^-9 +// = 10^-9 +// = 0.000000001 +// +// 13.a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. +// = (fractionalSeconds / 60) / 60 + 4000 +// = 0.000000001 / 3600 + 4000 +// +// 13.b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode). +// = trunc(fractionalHours) +// = trunc(0.000000001 / 3600 + 4000) +// = 4000 +// +// 13.c. Set remainder to fractionalHours - hours. +// = fractionalHours - hours +// = 0.000000001 / 3600 + 4000 - 4000 +// = 0.000000001 / 3600 +// +// From Temporal.Duration.prototype.total ( options ): +// +// 18. If unit is "hours", then let whole be roundResult.[[Hours]]. +// ... +// 24. Return whole + roundResult.[[Remainder]]. +// +// |whole| is 4000 and the remainder is (0.000000001 / 3600). +// +// 0.000000001 / 3600 +// = (1 / 10^9) / 3600 +// = (1 / 36) / 10^11 +// = 0.02777.... / 10^11 +// = 0.0000000000002777... +// +// 4000.0000000000002777... can't be represented exactly, the next best approximation +// is 4000.0000000000005. + +const expected = 4000.0000000000005; +assert.sameValue(expected, 4000.0000000000002777, "the float representation of the result is 4000.0000000000005"); + +// The next Number in direction -Infinity is less precise. +assert.sameValue(nextDown(expected), 4000, "the next Number in direction -Infinity is less precise"); + +// The next Number in direction +Infinity is less precise. +assert.sameValue(nextUp(expected), 4000.000000000001, "the next Number in direction +Infinity is less precise"); + +assert.sameValue(total, expected, "return value of total()"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js new file mode 100644 index 0000000000..2a73bb5ef0 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js @@ -0,0 +1,101 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + RoundDuration computes on exact mathematical values. +features: [Temporal] +---*/ + +// Return the next Number value in direction to +Infinity. +function nextUp(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? -1n : 1n); + return f64[0]; +} + +// Return the next Number value in direction to -Infinity. +function nextDown(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return -Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? 1n : -1n); + return f64[0]; +} + +let duration = Temporal.Duration.from({ + hours: 4000, + minutes: 59, + seconds: 59, + milliseconds: 999, + microseconds: 999, + nanoseconds: 999, +}); + +let total = duration.total({unit: "hours"}); + +// From RoundDuration(): +// +// 7. Let fractionalSeconds be nanoseconds × 10^-9 + microseconds × 10^-6 + milliseconds × 10^-3 + seconds. +// = 999 × 10^-9 + 999 × 10^-6 + 999 × 10^-3 + 59 +// = 59.999'999'999 +// +// 13.a. Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. +// = (59.999'999'999 / 60 + 59) / 60 + 4000 +// = 1 - 0.000000001 / 3600 + 4000 +// +// 13.b. Set hours to RoundNumberToIncrement(fractionalHours, increment, roundingMode). +// = trunc(fractionalHours) +// = trunc(1 - 0.000000001 / 3600 + 4000) +// = 4000 +// +// 13.c. Set remainder to fractionalHours - hours. +// = fractionalHours - hours +// = 1 - 0.000000001 / 3600 + 4000 - 4000 +// = 1 - 0.000000001 / 3600 +// +// From Temporal.Duration.prototype.total ( options ): +// +// 18. If unit is "hours", then let whole be roundResult.[[Hours]]. +// ... +// 24. Return whole + roundResult.[[Remainder]]. +// +// |whole| is 4000 and the remainder is (1 - 0.000000001 / 3600). +// +// 1 - 0.000000001 / 3600 +// = 1 - (1 / 10^9) / 3600 +// = 1 - (1 / 36) / 10^11 +// = 1 - 0.02777.... / 10^11 +// = 0.9999999999997222... +// +// 4000.9999999999997222... can't be represented exactly, the next best approximation +// is 4000.9999999999995. + +const expected = 4000.9999999999995; +assert.sameValue(expected, 4000.9999999999997222); + +// The next Number in direction -Infinity is less precise. +assert.sameValue(nextDown(expected), 4000.999999999999); + +// The next Number in direction +Infinity is less precise. +assert.sameValue(nextUp(expected), 4001); + +assert.sameValue(total, expected); + +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 new file mode 100644 index 0000000000..ab3fd772cc --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-3.js @@ -0,0 +1,121 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + RoundDuration computes in such a way as to avoid precision loss when the + computed day, week, month, and year lengths are very large numbers. +info: | + RoundDuration: + ... + 7. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then + a. If _zonedRelativeTo_ is not *undefined*, then + ... + iii. Let _fractionalDays_ be _days_ + _result_.[[Days]] + DivideNormalizedTimeDuration(_result_.[[Remainder]], _result_.[[DayLength]]). + ... + 10. If _unit_ is *"year"*, then + ... + z. Let _fractionalYears_ be _years_ + _fractionalDays_ / abs(_oneYearDays_). + ... + 11. If _unit_ is *"month"*, then + ... + z. Let _fractionalMonths_ be _months_ + _fractionalDays_ / abs(_oneMonthDays_). + ... + 12. If _unit_ is *"week"*, then + ... + s. Let _fractionalWeeks_ be _weeks_ + _fractionalDays_ / abs(_oneWeekDays_). +includes: [compareArray.js] +features: [Temporal] +---*/ + +// Return the next Number value in direction to +Infinity. +function nextUp(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? -1n : 1n); + return f64[0]; +} + +// Return the next Number value in direction to -Infinity. +function nextDown(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return -Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? 1n : -1n); + return f64[0]; +} + +// Return bit pattern representation of Number as a Uint8Array of bytes. +function f64Repr(f) { + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, f); + return new Uint8Array(buf); +} + +// ============ + +const tz = new (class extends Temporal.TimeZone { + getPossibleInstantsFor() { + // Called in NormalizedTimeDurationToDays 21.a 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)]; + } +})("UTC"); + +const cal = new (class extends Temporal.Calendar { + dateAdd() { + // Called in MoveRelativeDate from RoundDuration 10.x, 11.x, or 12.q. + // Sets _oneYearDays_, _oneMonthDays_, or _oneWeekDays_ respectively to + // 200_000_000, its largest possible value. + return new Temporal.PlainDate(275760, 9, 13); + } +})("iso8601"); + +const relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, tz, cal); +const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, 0, 0, 0, /* nanoseconds = */ 1); + +/* + * RoundDuration step 7: + * ii. result = { [[Days]] = 0, [[Remainder]] = normalized time duration of 1 ns, + * [[DayLength]] = Number.MAX_SAFE_INTEGER } + * iii. fractionalDays = 0 + 0 + 1 / Number.MAX_SAFE_INTEGER + * step 10: + * y. oneYearDays = 200_000_000 + * z. fractionalYears = 0 + (1 / Number.MAX_SAFE_INTEGER) / 200_000_000 + */ +// Calculated with Python's Decimal module to 50 decimal places +const expected = 5.55111512312578_3318415740544369642963189519987393e-25; + +// Check that we are not accidentally losing precision in our expected value: + +assert.sameValue(expected, 5.55111512312578_373662e-25, "the float representation of the result is 5.55111512312578373662e-25"); +assert.compareArray( + f64Repr(expected), + [0x3a, 0xe5, 0x79, 0x8e, 0xe2, 0x30, 0x8c, 0x3b], + "the bit representation of the result is 0x3ae5798ee2308c3b" +); +// The next Number in direction -Infinity is less precise. +assert.sameValue(nextDown(expected), 5.55111512312578_281826e-25, "the next Number in direction -Infinity is less precise"); +// The next Number in direction +Infinity is less precise. +assert.sameValue(nextUp(expected), 5.55111512312578_465497e-25, "the next Number in direction +Infinity is less precise"); + +assert.sameValue(d.total({ unit: "years", relativeTo }), expected, "Correct division by large number in years total"); +assert.sameValue(d.total({ unit: "months", relativeTo }), expected, "Correct division by large number in months total"); +assert.sameValue(d.total({ unit: "weeks", relativeTo }), expected, "Correct division by large number in weeks total"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js new file mode 100644 index 0000000000..74a77e669f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-4.js @@ -0,0 +1,155 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + RoundDuration computes in such a way as to avoid precision loss when the + computed day, week, month, and year lengths are very large numbers. +info: | + RoundDuration: + ... + 7. If _unit_ is one of *"year"*, *"month"*, *"week"*, or *"day"*, then + a. If _zonedRelativeTo_ is not *undefined*, then + ... + iii. Let _fractionalDays_ be _days_ + _result_.[[Days]] + DivideNormalizedTimeDuration(_result_.[[Remainder]], _result_.[[DayLength]]). + ... + 10. If _unit_ is *"year"*, then + ... + z. Let _fractionalYears_ be _years_ + _fractionalDays_ / abs(_oneYearDays_). + ... + 11. If _unit_ is *"month"*, then + ... + z. Let _fractionalMonths_ be _months_ + _fractionalDays_ / abs(_oneMonthDays_). + ... + 12. If _unit_ is *"week"*, then + ... + s. Let _fractionalWeeks_ be _weeks_ + _fractionalDays_ / abs(_oneWeekDays_). +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +// Return the next Number value in direction to +Infinity. +function nextUp(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? -1n : 1n); + return f64[0]; +} + +// Return the next Number value in direction to -Infinity. +function nextDown(num) { + if (!Number.isFinite(num)) { + return num; + } + if (num === 0) { + return -Number.MIN_VALUE; + } + + var f64 = new Float64Array([num]); + var u64 = new BigUint64Array(f64.buffer); + u64[0] += (num < 0 ? 1n : -1n); + return f64[0]; +} + +// Return bit pattern representation of Number as a Uint8Array of bytes. +function f64Repr(f) { + const buf = new ArrayBuffer(8); + new DataView(buf).setFloat64(0, f); + return new Uint8Array(buf); +} + +// ============ +// Set up contrived custom time zone and calendar to give dayLengthNs, +// oneYearDays, oneMonthDays, and oneWeekDays their largest possible values + +function createTimeZone() { + const tz = new Temporal.TimeZone("UTC"); + TemporalHelpers.substituteMethod(tz, "getPossibleInstantsFor", [ + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.e.ii AddDaysToZonedDateTime + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.i.ii NormalizedTimeDurationToDays step 16 + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.i.ii NormalizedTimeDurationToDays step 19 + [new Temporal.Instant(-86400_0000_0000_000_000_000n)], // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime + [new Temporal.Instant(-86400_0000_0000_000_000_000n + 2n ** 53n - 1n)], // RoundDuration step 7.a.ii NormalizedTimeDurationToDays step 19 + // sets dayLengthNs to Number.MAX_SAFE_INTEGER + ]); + return tz; +} + +function createCalendar() { + const cal = new Temporal.Calendar("iso8601"); + TemporalHelpers.substituteMethod(cal, "dateAdd", [ + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime + TemporalHelpers.SUBSTITUTE_SKIP, // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime + new Temporal.PlainDate(-271821, 4, 20), // RoundDuration step 10.d/11.d AddDate + new Temporal.PlainDate(-271821, 4, 20), // RoundDuration step 10.f/11.f AddDate + new Temporal.PlainDate(275760, 9, 13), // RoundDuration step 10.r/11.r MoveRelativeDate + // sets one{Year,Month,Week}Days to 200_000_000 + ]); + return cal; +} + +// ============ + +// We will calculate the total years/months/weeks of durations with: +// 1 year/month/week, 1 day, 0.199000001 s + +// RoundDuration step 7: +// ii. result = { [[Days]] = 1, [[Remainder]] = normalized time duration of 199_000_001 ns, +// [[DayLength]] = Number.MAX_SAFE_INTEGER } +// iii. fractionalDays = 1 + 0 + 199_000_001 / Number.MAX_SAFE_INTEGER +// step 10: (similar for steps 11 and 12 in the case of months/weeks) +// y. oneYearDays = 200_000_000 +// z. fractionalYears = 1 + (1 + 199_000_001 / Number.MAX_SAFE_INTEGER) / 200_000_000 +// +// Note: if calculated as 1 + 1/200_000_000 + 199_000_001 / Number.MAX_SAFE_INTEGER / 200_000_000 +// this will lose precision and give the result 1.000000005. + +// Calculated with Python's Decimal module to 50 decimal places +const expected = 1.000000005000000_1104671915053146003490515686745299; + +// Check that we are not accidentally losing precision in our expected value: + +assert.sameValue(expected, 1.000000005000000_2, "the float representation of the result is 1.0000000050000002"); +assert.compareArray( + f64Repr(expected), + [0x3f, 0xf0, 0x00, 0x00, 0x01, 0x57, 0x98, 0xef], + "the bit representation of the result is 0x3ff00000015798ef" +); +// The next Number in direction -Infinity is less precise. +assert.sameValue(nextDown(expected), 1.000000004999999_96961, "the next Number in direction -Infinity is less precise"); +// The next Number in direction +Infinity is less precise. +assert.sameValue(nextUp(expected), 1.000000005000000_4137, "the next Number in direction +Infinity is less precise"); + +// ============ + +let relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), createCalendar()); +const dYears = new Temporal.Duration(/* years = */ 1, 0, 0, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1); +assert.sameValue(dYears.total({ unit: "years", relativeTo }), expected, "Correct division by large number in years total"); + +relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), createCalendar()); +const dMonths = new Temporal.Duration(0, /* months = */ 1, 0, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1); +assert.sameValue(dMonths.total({ unit: "months", relativeTo }), expected, "Correct division by large number in months total"); + +// Weeks calculation doesn't have the AddDate calls to convert months/weeks to days +const weeksCal = new Temporal.Calendar("iso8601"); +TemporalHelpers.substituteMethod(weeksCal, "dateAdd", [ + TemporalHelpers.SUBSTITUTE_SKIP, // Duration.total step 19.a MoveRelativeZonedDateTime → AddZonedDateTime + TemporalHelpers.SUBSTITUTE_SKIP, // RoundDuration step 7.a.i MoveRelativeZonedDateTime → AddZonedDateTime + new Temporal.PlainDate(275760, 9, 13), // RoundDuration step 12.q MoveRelativeDate + // sets one{Year,Month,Week}Days to 200_000_000 +]); +relativeTo = new Temporal.ZonedDateTime(-86400_0000_0000_000_000_000n, createTimeZone(), weeksCal); +const dWeeks = new Temporal.Duration(0, 0, /* weejs = */ 1, /* days = */ 1, 0, 0, 0, /* milliseconds = */ 199, 0, /* nanoseconds = */ 1); +assert.sameValue(dWeeks.total({ unit: "weeks", relativeTo }), expected, "Correct division by large number in weeks total"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js new file mode 100644 index 0000000000..9d315db9c2 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-5.js @@ -0,0 +1,61 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: BalanceTimeDuration computes on exact mathematical values. +features: [BigInt, Temporal] +---*/ + +const seconds = 8692288669465520; + +{ + const milliseconds = 513; + const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, milliseconds); + + const result = d.total({ unit: "milliseconds" }); + + // The result should be the nearest Number value to 8692288669465520512 + const expectedMilliseconds = Number(BigInt(seconds) * 1000n + BigInt(milliseconds)); + assert.sameValue(expectedMilliseconds, 8692288669465520_513, "check expected value (ms)"); + + assert.sameValue( + result, expectedMilliseconds, + "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit milliseconds" + ); +} + +{ + const microseconds = 373761; + const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, 0, microseconds); + + const result = d.total({ unit: "microseconds" }); + + // The result should be the nearest Number value to 8692288669465520373761 + const expectedMicroseconds = Number(BigInt(seconds) * 1_000_000n + BigInt(microseconds)); + assert.sameValue(expectedMicroseconds, 8692288669465520_373_761, "check expected value (µs)"); + + assert.sameValue( + result, expectedMicroseconds, + "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit milliseconds" + ); +} + +{ + const nanoseconds = 321_414_345; + const d = new Temporal.Duration(0, 0, 0, 0, 0, 0, seconds, 0, 0, nanoseconds); + + const result = d.total({ unit: "nanoseconds" }); + + // The result should be the nearest Number value to 8692288669465520321414345 + const expectedNanoseconds = Number(BigInt(seconds) * 1_000_000_000n + BigInt(nanoseconds)); + assert.sameValue(expectedNanoseconds, 8692288669465520_321_414_345, "check expected value (ns)"); + + assert.sameValue( + result, expectedNanoseconds, + "BalanceTimeDuration should implement floating-point calculation correctly for largestUnit nanoseconds" + ); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js new file mode 100644 index 0000000000..8072451c5f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js @@ -0,0 +1,143 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + DivideNormalizedTimeDuration computes on exact mathematical values. +info: | + Temporal.Duration.prototype.total ( totalOf ) + + ... + 20. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], + unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], days, norm, 1, + unit, "trunc", plainRelativeTo, calendarRec, zonedRelativeTo, timeZoneRec, + precalculatedPlainDateTime). + 21. Return 𝔽(roundRecord.[[Total]]). + + RoundDuration ( ... ) + + ... + 14. Else if unit is "hour", then + a. Let divisor be 3.6 × 10^12. + b. Set total to DivideNormalizedTimeDuration(norm, divisor). + ... + + DivideNormalizedTimeDuration ( d, divisor ) + + 1. Assert: divisor ≠ 0. + 2. Return d.[[TotalNanoseconds]] / divisor. +features: [Temporal] +---*/ + +// Randomly generated test data. +const data = [ + { + hours: 816, + nanoseconds: 2049_187_497_660, + }, + { + hours: 7825, + nanoseconds: 1865_665_040_770, + }, + { + hours: 0, + nanoseconds: 1049_560_584_034, + }, + { + hours: 2055144, + nanoseconds: 2502_078_444_371, + }, + { + hours: 31, + nanoseconds: 1010_734_758_745, + }, + { + hours: 24, + nanoseconds: 2958_999_560_387, + }, + { + hours: 0, + nanoseconds: 342_058_521_588, + }, + { + hours: 17746, + nanoseconds: 3009_093_506_309, + }, + { + hours: 4, + nanoseconds: 892_480_914_569, + }, + { + hours: 3954, + nanoseconds: 571_647_777_618, + }, + { + hours: 27, + nanoseconds: 2322_199_502_640, + }, + { + hours: 258054064, + nanoseconds: 2782_411_891_222, + }, + { + hours: 1485, + nanoseconds: 2422_559_903_100, + }, + { + hours: 0, + nanoseconds: 1461_068_214_153, + }, + { + hours: 393, + nanoseconds: 1250_229_561_658, + }, + { + hours: 0, + nanoseconds: 91_035_820, + }, + { + hours: 0, + nanoseconds: 790_982_655, + }, + { + hours: 150, + nanoseconds: 608_531_524, + }, + { + hours: 5469, + nanoseconds: 889_204_952, + }, + { + hours: 7870, + nanoseconds: 680_042_770, + }, +]; + +const nsPerHour = 3600_000_000_000; + +const fractionDigits = Math.log10(nsPerHour) + Math.log10(100_000_000_000) - Math.log10(36); +assert.sameValue(fractionDigits, 22); + +for (let {hours, nanoseconds} of data) { + assert(nanoseconds < nsPerHour); + + // Compute enough fractional digits to approximate the exact result. Use BigInts + // to avoid floating point precision loss. Fill to the left with implicit zeros. + let fraction = ((BigInt(nanoseconds) * 100_000_000_000n) / 36n).toString().padStart(fractionDigits, "0"); + + // Get the Number approximation from the string representation. + let expected = Number(`${hours}.${fraction}`); + + let d = Temporal.Duration.from({hours, nanoseconds}); + let actual = d.total("hours"); + + assert.sameValue( + actual, + expected, + `hours=${hours}, nanoseconds=${nanoseconds}`, + ); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js new file mode 100644 index 0000000000..6096f1afac --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js @@ -0,0 +1,122 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2024 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + DivideNormalizedTimeDuration computes on exact mathematical values. +info: | + Temporal.Duration.prototype.total ( totalOf ) + + ... + 20. Let roundRecord be ? RoundDuration(unbalanceResult.[[Years]], + unbalanceResult.[[Months]], unbalanceResult.[[Weeks]], days, norm, 1, + unit, "trunc", plainRelativeTo, calendarRec, zonedRelativeTo, timeZoneRec, + precalculatedPlainDateTime). + 21. Return 𝔽(roundRecord.[[Total]]). + + RoundDuration ( ... ) + + ... + 16. Else if unit is "second", then + a. Let divisor be 10^9. + b. Set total to DivideNormalizedTimeDuration(norm, divisor). + ... + 17. Else if unit is "millisecond", then + a. Let divisor be 10^6. + b. Set total to DivideNormalizedTimeDuration(norm, divisor). + ... + 18. Else if unit is "microsecond", then + a. Let divisor be 10^3. + b. Set total to DivideNormalizedTimeDuration(norm, divisor). + ... + + DivideNormalizedTimeDuration ( d, divisor ) + + 1. Assert: divisor ≠ 0. + 2. Return d.[[TotalNanoseconds]] / divisor. +features: [Temporal] +---*/ + +// Test duration units where the fractional part is a power of ten. +const units = [ + "seconds", "milliseconds", "microseconds", "nanoseconds", +]; + +// Conversion factors to nanoseconds precision. +const toNanos = { + "seconds": 1_000_000_000n, + "milliseconds": 1_000_000n, + "microseconds": 1_000n, + "nanoseconds": 1n, +}; + +const integers = [ + // Small integers. + 0, + 1, + 2, + + // Large integers around Number.MAX_SAFE_INTEGER. + 2**51, + 2**52, + 2**53, + 2**54, +]; + +const fractions = [ + // True fractions. + 0, 1, 10, 100, 125, 200, 250, 500, 750, 800, 900, 950, 999, + + // Fractions with overflow. + 1_000, + 1_999, + 2_000, + 2_999, + 3_000, + 3_999, + 4_000, + 4_999, + + 999_999, + 1_000_000, + 1_000_001, + + 999_999_999, + 1_000_000_000, + 1_000_000_001, +]; + +const maxTimeDuration = (2n ** 53n) * (10n ** 9n) - 1n; + +// Iterate over all units except the last one. +for (let unit of units.slice(0, -1)) { + let smallerUnit = units[units.indexOf(unit) + 1]; + + for (let integer of integers) { + for (let fraction of fractions) { + // Total nanoseconds must not exceed |maxTimeDuration|. + let totalNanoseconds = BigInt(integer) * toNanos[unit] + BigInt(fraction) * toNanos[smallerUnit]; + if (totalNanoseconds > maxTimeDuration) { + continue; + } + + // Get the Number approximation from the string representation. + let i = BigInt(integer) + BigInt(fraction) / 1000n; + let f = String(fraction % 1000).padStart(3, "0"); + let expected = Number(`${i}.${f}`); + + let d = Temporal.Duration.from({[unit]: integer, [smallerUnit]: fraction}); + let actual = d.total(unit); + + assert.sameValue( + actual, + expected, + `${unit}=${integer}, ${smallerUnit}=${fraction}`, + ); + } + } +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js new file mode 100644 index 0000000000..8374803e9f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/prop-desc.js @@ -0,0 +1,24 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: The "total" property of Temporal.Duration.prototype +includes: [propertyHelper.js] +features: [Temporal] +---*/ + +assert.sameValue( + typeof Temporal.Duration.prototype.total, + "function", + "`typeof Duration.prototype.total` is `function`" +); + +verifyProperty(Temporal.Duration.prototype, "total", { + writable: true, + enumerable: false, + configurable: true, +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js new file mode 100644 index 0000000000..b40b2d4b30 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/proto-in-calendar-fields.js @@ -0,0 +1,17 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. +/*--- +esid: sec-temporal.duration.prototype.total +description: If a calendar's fields() method returns a field named '__proto__', PrepareTemporalFields should throw a RangeError. +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarWithExtraFields(['__proto__']); +const relativeTo = { year: 2023, month: 5, monthCode: 'M05', day: 1, calendar: calendar, timeZone: 'Europe/Paris' }; +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo })); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js new file mode 100644 index 0000000000..0e80cd0ef5 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/read-time-fields-before-datefromfields.js @@ -0,0 +1,24 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: The time fields are read from the object before being passed to dateFromFields(). +info: | + sec-temporal.duration.prototype.total step 4: + 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_). + sec-temporal-torelativetemporalobject step 4.g: + g. Let _result_ be ? InterpretTemporalDateTimeFields(_calendar_, _fields_, _options_). + sec-temporal-interprettemporaldatetimefields steps 1–2: + 1. Let _timeResult_ be ? ToTemporalTimeRecord(_fields_). + 2. Let _temporalDate_ be ? DateFromFields(_calendar_, _fields_, _options_). +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarMakeInvalidGettersTime(); +const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1); +duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, calendar } }); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js new file mode 100644 index 0000000000..9567656f64 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-infinity-throws-rangeerror.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: Throws if any value in the property bag is Infinity or -Infinity +esid: sec-temporal.duration.prototype.total +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); +const base = { year: 2000, month: 5, day: 2, hour: 15, minute: 30, second: 45, millisecond: 987, microsecond: 654, nanosecond: 321 }; + +[Infinity, -Infinity].forEach((inf) => { + ["year", "month", "day", "hour", "minute", "second", "millisecond", "microsecond", "nanosecond"].forEach((prop) => { + assert.throws(RangeError, () => instance.total({ unit: "seconds", relativeTo: { ...base, [prop]: inf } }), `${prop} property cannot be ${inf} in relativeTo`); + + const calls = []; + const obj = TemporalHelpers.toPrimitiveObserver(calls, inf, prop); + assert.throws(RangeError, () => instance.total({ unit: "seconds", relativeTo: { ...base, [prop]: obj } })); + assert.compareArray(calls, [`get ${prop}.valueOf`, `call ${prop}.valueOf`], "it fails after fetching the primitive value"); + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js new file mode 100644 index 0000000000..c69a1dd35e --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-leap-second.js @@ -0,0 +1,45 @@ +// |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.total +description: Leap second is constrained in both an ISO string and a property bag +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let relativeTo = "2016-12-31T23:59:60"; +const result1 = instance.total({ unit: "days", relativeTo }); +assert.sameValue( + result1, + 366, + "leap second is a valid ISO string for PlainDate relativeTo" +); + +relativeTo = "2016-12-31T23:59:60+00:00[UTC]"; +const result2 = instance.total({ unit: "days", relativeTo }); +assert.sameValue( + result2, + 366, + "leap second is a valid ISO string for ZonedDateTime relativeTo" +); + +relativeTo = { year: 2016, month: 12, day: 31, hour: 23, minute: 59, second: 60 }; +const result3 = instance.total({ unit: "days", relativeTo }); +assert.sameValue( + result3, + 366, + "second: 60 is valid in a property bag for PlainDate relativeTo" +); + +relativeTo = { year: 2016, month: 12, day: 31, hour: 23, minute: 59, second: 60, timeZone: "UTC" }; +const result4 = instance.total({ unit: "days", relativeTo }); +assert.sameValue( + result4, + 366, + "second: 60 is valid in a property bag for ZonedDateTime relativeTo" +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js new file mode 100644 index 0000000000..765099da48 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-number.js @@ -0,0 +1,28 @@ +// |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.total +description: A number cannot be used in place of a relativeTo +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +const numbers = [ + 1, + 20191101, + -20191101, + 1234567890, +]; + +for (const relativeTo of numbers) { + assert.throws( + TypeError, + () => instance.total({ unit: "days", relativeTo }), + `A number (${relativeTo}) is not a valid ISO string for relativeTo` + ); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js new file mode 100644 index 0000000000..d1bdde5edf --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-plaindate-add24hourdaystonormalizedtimeduration-out-of-range.js @@ -0,0 +1,19 @@ +// |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.compare +description: RangeError thrown if adding the duration to the relativeTo date would result in anout-of-range date-time +features: [Temporal] +---*/ + +let duration = Temporal.Duration.from({ + years: 1, + seconds: 2**53 - 1, +}); +let relativeTo = new Temporal.PlainDate(2000, 1, 1); + +assert.throws(RangeError, () => duration.total({ relativeTo, unit: "days" })); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js new file mode 100644 index 0000000000..965d3e8813 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-ambiguous-wall-clock-time.js @@ -0,0 +1,92 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Correct time zone calls are made when converting a ZonedDateTime-like + relativeTo property bag denoting an ambiguous wall-clock time +includes: [temporalHelpers.js, compareArray.js] +features: [Temporal] +---*/ + +const actual = []; + +const dstTimeZone = TemporalHelpers.springForwardFallBackTimeZone(); +const dstTimeZoneObserver = TemporalHelpers.timeZoneObserver(actual, "timeZone", { + getOffsetNanosecondsFor: dstTimeZone.getOffsetNanosecondsFor.bind(dstTimeZone), + getPossibleInstantsFor: dstTimeZone.getPossibleInstantsFor.bind(dstTimeZone), +}); +const calendar = TemporalHelpers.calendarObserver(actual, "calendar"); + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let relativeTo = { year: 2000, month: 4, day: 2, hour: 2, minute: 30, timeZone: dstTimeZoneObserver, calendar }; +instance.total({ unit: "days", relativeTo }); + +const expected = [ + // GetTemporalCalendarSlotValueWithISODefault + "has calendar.dateAdd", + "has calendar.dateFromFields", + "has calendar.dateUntil", + "has calendar.day", + "has calendar.dayOfWeek", + "has calendar.dayOfYear", + "has calendar.daysInMonth", + "has calendar.daysInWeek", + "has calendar.daysInYear", + "has calendar.fields", + "has calendar.id", + "has calendar.inLeapYear", + "has calendar.mergeFields", + "has calendar.month", + "has calendar.monthCode", + "has calendar.monthDayFromFields", + "has calendar.monthsInYear", + "has calendar.weekOfYear", + "has calendar.year", + "has calendar.yearMonthFromFields", + "has calendar.yearOfWeek", + // lookup + "get calendar.dateFromFields", + "get calendar.fields", + // CalendarFields + "call calendar.fields", + // InterpretTemporalDateTimeFields + "call calendar.dateFromFields", + // ToTemporalTimeZoneSlotValue + "has timeZone.getOffsetNanosecondsFor", + "has timeZone.getPossibleInstantsFor", + "has timeZone.id", + // lookup + "get timeZone.getOffsetNanosecondsFor", + "get timeZone.getPossibleInstantsFor", + // InterpretISODateTimeOffset + "call timeZone.getPossibleInstantsFor", +]; + +const expectedSpringForward = expected.concat([ + // DisambiguatePossibleInstants + "call timeZone.getOffsetNanosecondsFor", + "call timeZone.getOffsetNanosecondsFor", + "call timeZone.getPossibleInstantsFor", +]); +assert.compareArray( + actual.slice(0, expectedSpringForward.length), // ignore operations after ToRelativeTemporalObject + expectedSpringForward, + "order of operations converting property bag at skipped wall-clock time" +); +actual.splice(0); // clear + +relativeTo = { year: 2000, month: 10, day: 29, hour: 1, minute: 30, timeZone: dstTimeZoneObserver, calendar }; +instance.total({ unit: "days", relativeTo }); + +assert.compareArray( + actual.slice(0, expected.length), // ignore operations after ToRelativeTemporalObject + expected, + "order of operations converting property bag at repeated wall-clock time" +); +actual.splice(0); // clear + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js new file mode 100644 index 0000000000..44c97b82e9 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-builtin-calendar-no-array-iteration.js @@ -0,0 +1,25 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Calling the method with a relativeTo property bag with a builtin calendar + causes no observable array iteration when getting the calendar fields. +features: [Temporal] +---*/ + +const arrayPrototypeSymbolIteratorOriginal = Array.prototype[Symbol.iterator]; +Array.prototype[Symbol.iterator] = function arrayIterator() { + throw new Test262Error("Array should not be iterated"); +} + +const timeZone = "UTC"; +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +const relativeTo = { year: 2000, month: 5, day: 2, hour: 21, minute: 43, second: 5, timeZone, calendar: "iso8601" }; +instance.total({ unit: "days", relativeTo }); + +Array.prototype[Symbol.iterator] = arrayPrototypeSymbolIteratorOriginal; + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js new file mode 100644 index 0000000000..d8a4d09f9d --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-datefromfields-called-with-null-prototype-fields.js @@ -0,0 +1,19 @@ +// |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.total +description: > + Calendar.dateFromFields method is called with a null-prototype fields object +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const calendar = TemporalHelpers.calendarCheckFieldsPrototypePollution(); +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +const relativeTo = { year: 2000, month: 5, day: 2, calendar }; +instance.total({ unit: "days", relativeTo }); +assert.sameValue(calendar.dateFromFieldsCallCount, 1, "dateFromFields should be called on the property bag's calendar"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js new file mode 100644 index 0000000000..52a4647bcf --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-number.js @@ -0,0 +1,29 @@ +// |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.total +description: A number as calendar in relativeTo property bag is invalid +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +const numbers = [ + 1, + 19970327, + -19970327, + 1234567890, +]; + +for (const calendar of numbers) { + const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar }; + assert.throws( + TypeError, + () => instance.total({ unit: "days", relativeTo }), + `A number (${calendar}) is not a valid ISO string for relativeTo.calendar` + ); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js new file mode 100644 index 0000000000..8cba83ba56 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-string.js @@ -0,0 +1,29 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Builtin dateFromFields method is not observably called when the property bag + has a string-valued calendar property +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const dateFromFieldsOriginal = Object.getOwnPropertyDescriptor(Temporal.Calendar.prototype, "dateFromFields"); +Object.defineProperty(Temporal.Calendar.prototype, "dateFromFields", { + configurable: true, + enumerable: false, + get() { + TemporalHelpers.assertUnreachable("dateFromFields should not be looked up"); + }, +}); + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +const relativeTo = { year: 2000, month: 5, day: 2, calendar: "iso8601" }; +instance.total({ unit: "days", relativeTo }); + +Object.defineProperty(Temporal.Calendar.prototype, "dateFromFields", dateFromFieldsOriginal); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js new file mode 100644 index 0000000000..cef7d31b45 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-calendar-wrong-type.js @@ -0,0 +1,48 @@ +// |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.total +description: > + Appropriate error thrown when relativeTo.calendar cannot be converted to a + calendar object or string +features: [BigInt, Symbol, Temporal] +---*/ + +const timeZone = new Temporal.TimeZone("UTC"); +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +const primitiveTests = [ + [null, "null"], + [true, "boolean"], + ["", "empty string"], + [1, "number that doesn't convert to a valid ISO string"], + [1n, "bigint"], +]; + +for (const [calendar, description] of primitiveTests) { + const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar }; + assert.throws( + typeof calendar === 'string' ? RangeError : TypeError, + () => instance.total({ unit: "days", relativeTo }), + `${description} does not convert to a valid ISO string` + ); +} + +const typeErrorTests = [ + [Symbol(), "symbol"], + [{}, "plain object that doesn't implement the protocol"], + [new Temporal.TimeZone("UTC"), "time zone instance"], + [Temporal.PlainDate, "Temporal.PlainDate, object"], + [Temporal.PlainDate.prototype, "Temporal.PlainDate.prototype, object"], + [Temporal.ZonedDateTime, "Temporal.ZonedDateTime, object"], + [Temporal.ZonedDateTime.prototype, "Temporal.ZonedDateTime.prototype, object"], +]; + +for (const [calendar, description] of typeErrorTests) { + const relativeTo = { year: 2019, monthCode: "M11", day: 1, calendar }; + assert.throws(TypeError, () => instance.total({ unit: "days", relativeTo }), `${description} is not a valid property bag and does not convert to a string`); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js new file mode 100644 index 0000000000..16ab8873ec --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-getpossibleinstantsfor-called-with-iso8601-calendar.js @@ -0,0 +1,59 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Time zone's getPossibleInstantsFor is called with a PlainDateTime with the + built-in ISO 8601 calendar +features: [Temporal] +info: | + DisambiguatePossibleInstants: + 2. Let _n_ be _possibleInstants_'s length. + ... + 5. Assert: _n_ = 0. + ... + 19. If _disambiguation_ is *"earlier"*, then + ... + c. Let _earlierDateTime_ be ! CreateTemporalDateTime(..., *"iso8601"*). + d. Set _possibleInstants_ to ? GetPossibleInstantsFor(_timeZone_, _earlierDateTime_). + ... + 20. Assert: _disambiguation_ is *"compatible"* or *"later"*. + ... + 23. Let _laterDateTime_ be ! CreateTemporalDateTime(..., *"iso8601"*). + 24. Set _possibleInstants_ to ? GetPossibleInstantsFor(_timeZone_, _laterDateTime_). +---*/ + +class SkippedDateTime extends Temporal.TimeZone { + constructor() { + super("UTC"); + this.calls = 0; + } + + getPossibleInstantsFor(dateTime) { + // Calls occur in pairs. For the first one return no possible instants so + // that DisambiguatePossibleInstants will call it again + if (this.calls++ % 2 == 0) { + return []; + } + + assert.sameValue( + dateTime.getISOFields().calendar, + "iso8601", + "getPossibleInstantsFor called with dateTime with built-in ISO 8601 calendar" + ); + return super.getPossibleInstantsFor(dateTime); + } +} + +const nonBuiltinISOCalendar = new Temporal.Calendar("iso8601"); +const timeZone = new SkippedDateTime(); +const relativeTo = { year: 2000, month: 5, day: 2, timeZone, calendar: nonBuiltinISOCalendar }; + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +instance.total({ unit: "days", relativeTo }); + +assert.sameValue(timeZone.calls, 10, "getPossibleInstantsFor should have been called 10 times"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js new file mode 100644 index 0000000000..dcb4944855 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-invalid-offset-string.js @@ -0,0 +1,32 @@ +// |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.total +description: relativeTo property bag with offset property is rejected if offset is in the wrong format +features: [Temporal] +---*/ + +const timeZone = new Temporal.TimeZone("UTC"); +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +const badOffsets = [ + "00:00", // missing sign + "+0", // too short + "-000:00", // too long + 0, // must be a string + null, // must be a string + true, // must be a string + 1000n, // must be a string +]; +badOffsets.forEach((offset) => { + const relativeTo = { year: 2021, month: 10, day: 28, offset, timeZone }; + assert.throws( + typeof(offset) === 'string' ? RangeError : TypeError, + () => instance.total({ unit: "days", relativeTo }), + `"${offset} is not a valid offset string` + ); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js new file mode 100644 index 0000000000..dc9d54ed5d --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-no-time-units.js @@ -0,0 +1,17 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Missing time units in relativeTo property bag default to 0 +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let relativeTo = { year: 2000, month: 1, day: 1 }; +const result = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result, 367, "missing time units default to 0"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js new file mode 100644 index 0000000000..d5200d49b8 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-non-integer.js @@ -0,0 +1,18 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: RangeError thrown if time zone reports an offset that is not an integer number of nanoseconds +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[3600_000_000_000.5, NaN, -Infinity, Infinity].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js new file mode 100644 index 0000000000..4929986c8c --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-not-callable.js @@ -0,0 +1,22 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: TypeError thrown if timeZone.getOffsetNanosecondsFor is not callable +features: [BigInt, Symbol, Temporal, arrow-function] +---*/ + +[undefined, null, true, Math.PI, 'string', Symbol('sym'), 42n, {}].forEach((notCallable) => { + const timeZone = new Temporal.TimeZone("UTC"); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + timeZone.getOffsetNanosecondsFor = notCallable; + assert.throws( + TypeError, + () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } }), + `Uncallable ${notCallable === null ? 'null' : typeof notCallable} getOffsetNanosecondsFor should throw TypeError` + ); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js new file mode 100644 index 0000000000..332fa1bec5 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-out-of-range.js @@ -0,0 +1,18 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: RangeError thrown if time zone reports an offset that is out of range +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[-86400_000_000_000, 86400_000_000_000].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js new file mode 100644 index 0000000000..323e64cc8f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-getoffsetnanosecondsfor-wrong-type.js @@ -0,0 +1,27 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: TypeError thrown if time zone reports an offset that is not a Number +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[ + undefined, + null, + true, + "+01:00", + Symbol(), + 3600_000_000_000n, + {}, + { valueOf() { return 3600_000_000_000; } }, +].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + assert.throws(TypeError, () => duration.total({ unit: "seconds", relativeTo: { year: 2000, month: 5, day: 2, hour: 12, timeZone } })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js new file mode 100644 index 0000000000..d5fa6c32ef --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-datetime.js @@ -0,0 +1,66 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Conversion of ISO date-time strings to Temporal.TimeZone instances +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1); + +let timeZone = "2021-08-19T17:30"; +assert.throws(RangeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "bare date-time string is not a time zone"); + +[ + "2021-08-19T17:30-07:00:01", + "2021-08-19T17:30-07:00:00", + "2021-08-19T17:30-07:00:00.1", + "2021-08-19T17:30-07:00:00.0", + "2021-08-19T17:30-07:00:00.01", + "2021-08-19T17:30-07:00:00.00", + "2021-08-19T17:30-07:00:00.001", + "2021-08-19T17:30-07:00:00.000", + "2021-08-19T17:30-07:00:00.0001", + "2021-08-19T17:30-07:00:00.0000", + "2021-08-19T17:30-07:00:00.00001", + "2021-08-19T17:30-07:00:00.00000", + "2021-08-19T17:30-07:00:00.000001", + "2021-08-19T17:30-07:00:00.000000", + "2021-08-19T17:30-07:00:00.0000001", + "2021-08-19T17:30-07:00:00.0000000", + "2021-08-19T17:30-07:00:00.00000001", + "2021-08-19T17:30-07:00:00.00000000", + "2021-08-19T17:30-07:00:00.000000001", + "2021-08-19T17:30-07:00:00.000000000", +].forEach((timeZone) => { + assert.throws( + RangeError, + () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), + `ISO string ${timeZone} with a sub-minute offset is not a valid time zone` + ); +}); + +// The following are all valid strings so should not throw: + +[ + "2021-08-19T17:30Z", + "2021-08-19T1730Z", + "2021-08-19T17:30-07:00", + "2021-08-19T1730-07:00", + "2021-08-19T17:30-0700", + "2021-08-19T1730-0700", + "2021-08-19T17:30[UTC]", + "2021-08-19T1730[UTC]", + "2021-08-19T17:30Z[UTC]", + "2021-08-19T1730Z[UTC]", + "2021-08-19T17:30-07:00[UTC]", + "2021-08-19T1730-07:00[UTC]", + "2021-08-19T17:30-0700[UTC]", + "2021-08-19T1730-0700[UTC]", +].forEach((timeZone) => { + instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js new file mode 100644 index 0000000000..a77fbb0f78 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-leap-second.js @@ -0,0 +1,22 @@ +// |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.total +description: Leap second is a valid ISO string for TimeZone +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1); +let timeZone = "2016-12-31T23:59:60+00:00[UTC]"; + +// A string with a leap second is a valid ISO string, so the following +// operation should not throw + +instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }); + +timeZone = "2021-08-19T17:30:45.123456789+23:59[+23:59:60]"; +assert.throws(RangeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), "leap second in time zone name not valid"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js new file mode 100644 index 0000000000..2592d5267a --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string-year-zero.js @@ -0,0 +1,24 @@ +// |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.total +description: Negative zero, as an extended year, is rejected +features: [Temporal, arrow-function] +---*/ + +const invalidStrings = [ + "-000000-10-31T17:45Z", + "-000000-10-31T17:45+00:00[UTC]", +]; +const instance = new Temporal.Duration(1); +invalidStrings.forEach((timeZone) => { + assert.throws( + RangeError, + () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), + "reject minus zero as extended year" + ); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js new file mode 100644 index 0000000000..bc64d7156c --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-string.js @@ -0,0 +1,40 @@ +// |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.total +description: Time zone IDs are valid input for a time zone +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const getPossibleInstantsForOriginal = Object.getOwnPropertyDescriptor(Temporal.TimeZone.prototype, "getPossibleInstantsFor"); +Object.defineProperty(Temporal.TimeZone.prototype, "getPossibleInstantsFor", { + configurable: true, + enumerable: false, + get() { + TemporalHelpers.assertUnreachable("getPossibleInstantsFor should not be looked up"); + }, +}); +const getOffsetNanosecondsForOriginal = Object.getOwnPropertyDescriptor(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor"); +Object.defineProperty(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor", { + configurable: true, + enumerable: false, + get() { + TemporalHelpers.assertUnreachable("getOffsetNanosecondsFor should not be looked up"); + }, +}); + +const instance = new Temporal.Duration(1); + +// The following are all valid strings so should not throw: + +["UTC", "+01:00"].forEach((timeZone) => { + instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }); +}); + +Object.defineProperty(Temporal.TimeZone.prototype, "getPossibleInstantsFor", getPossibleInstantsForOriginal); +Object.defineProperty(Temporal.TimeZone.prototype, "getOffsetNanosecondsFor", getOffsetNanosecondsForOriginal); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js new file mode 100644 index 0000000000..1f2d79fa47 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-propertybag-timezone-wrong-type.js @@ -0,0 +1,42 @@ +// |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.total +description: > + Appropriate error thrown when argument cannot be converted to a valid string + or object for TimeZone +features: [BigInt, Symbol, Temporal] +---*/ + +const instance = new Temporal.Duration(1); + +const primitiveTests = [ + [null, "null"], + [true, "boolean"], + ["", "empty string"], + [1, "number that doesn't convert to a valid ISO string"], + [19761118, "number that would convert to a valid ISO string in other contexts"], + [1n, "bigint"], +]; + +for (const [timeZone, description] of primitiveTests) { + assert.throws( + typeof timeZone === 'string' ? RangeError : TypeError, + () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), + `${description} does not convert to a valid ISO string` + ); +} + +const typeErrorTests = [ + [Symbol(), "symbol"], + [{}, "object not implementing time zone protocol"], + [new Temporal.Calendar("iso8601"), "calendar instance"], +]; + +for (const [timeZone, description] of typeErrorTests) { + assert.throws(TypeError, () => instance.total({ unit: "months", relativeTo: { year: 2000, month: 5, day: 2, timeZone } }), `${description} is not a valid object and does not convert to a string`); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js new file mode 100644 index 0000000000..b2e0cd8a56 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-datetime.js @@ -0,0 +1,40 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + Conversion of ISO date-time strings as relativeTo option to + Temporal.ZonedDateTime or Temporal.PlainDateTime instances +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let relativeTo = "2019-11-01T00:00"; +const result1 = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result1, 367, "bare date-time string is a plain relativeTo"); + +relativeTo = "2019-11-01T00:00-07:00"; +const result2 = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result2, 367, "date-time + offset is a plain relativeTo"); + +relativeTo = "2019-11-01T00:00[-07:00]"; +const result3 = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result3, 367, "date-time + IANA annotation is a zoned relativeTo"); + +relativeTo = "2019-11-01T00:00Z[-07:00]"; +const result4 = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result4, 367, "date-time + Z + IANA annotation is a zoned relativeTo"); + +relativeTo = "2019-11-01T00:00+00:00[UTC]"; +const result5 = instance.total({ unit: "days", relativeTo }); +assert.sameValue(result5, 367, "date-time + offset + IANA annotation is a zoned relativeTo"); + +relativeTo = "2019-11-01T00:00Z"; +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "date-time + Z throws without an IANA annotation"); +relativeTo = "2019-11-01T00:00+04:15[UTC]"; +assert.throws(RangeError, () => instance.total({ unit: "days", relativeTo }), "date-time + offset + IANA annotation throws if wall time and exact time mismatch"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js new file mode 100644 index 0000000000..f60e10f831 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-invalid.js @@ -0,0 +1,16 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: RangeError thrown if relativeTo is a string with the wrong format +features: [Temporal] +---*/ + +['bad string', '15:30:45.123456', 'iso8601', 'UTC', 'P1YT1H'].forEach((relativeTo) => { + const duration = new Temporal.Duration(0, 0, 0, 31); + assert.throws(RangeError, () => duration.total({ unit: "months", relativeTo })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js new file mode 100644 index 0000000000..d3d544c220 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime-invalid.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Throws a RangeError if "relativeTo" is a date/time value outside the valid limits. +info: | + Temporal.Duration.prototype.total ( totalOf ) + ... + 6. Let relativeTo be ? ToRelativeTemporalObject(totalOf). + ... + + ToRelativeTemporalObject ( options ) + ... + 9. Return ? CreateTemporalDate(result.[[Year]], result.[[Month]], result.[[Day]], calendar). +features: [Temporal] +---*/ + +var duration = Temporal.Duration.from({nanoseconds: 0}); +var options = {unit: "nanoseconds", relativeTo: "+999999-01-01"}; + +assert.throws(RangeError, () => duration.total(options)); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js new file mode 100644 index 0000000000..8f5a2c3f94 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-plaindatetime.js @@ -0,0 +1,17 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: The relativeTo option accepts a PlainDateTime-like ISO 8601 string +features: [Temporal] +---*/ + +['2000-01-01', '2000-01-01T00:00', '2000-01-01T00:00[u-ca=iso8601]'].forEach((relativeTo) => { + const duration = new Temporal.Duration(0, 0, 0, 31); + const result = duration.total({ unit: "months", relativeTo }); + assert.sameValue(result, 1); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js new file mode 100644 index 0000000000..10b00920d9 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime-wrong-offset.js @@ -0,0 +1,19 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Throws if a ZonedDateTime-like relativeTo string has the wrong UTC offset +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); +const relativeTo = "2000-01-01T00:00+05:30[UTC]"; +assert.throws( + RangeError, + () => instance.total({ unit: "days", relativeTo }), + "total should throw RangeError on a string with UTC offset mismatch" +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js new file mode 100644 index 0000000000..c23b3e97a4 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-string-zoneddatetime.js @@ -0,0 +1,22 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: The relativeTo option accepts a ZonedDateTime-like ISO 8601 string +features: [Temporal] +---*/ + +[ + '2000-01-01[UTC]', + '2000-01-01T00:00[UTC]', + '2000-01-01T00:00+00:00[UTC]', + '2000-01-01T00:00+00:00[UTC][u-ca=iso8601]', +].forEach((relativeTo) => { + const duration = new Temporal.Duration(0, 0, 0, 31); + const result = duration.total({ unit: "months", relativeTo }); + assert.sameValue(result, 1); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js new file mode 100644 index 0000000000..46f3849688 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-sub-minute-offset.js @@ -0,0 +1,29 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: relativeTo string accepts trailing zeroes in sub-minute UTC offset +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let result; +let relativeTo; + +const action = (relativeTo) => instance.total({ unit: "days", relativeTo }); + +relativeTo = "1970-01-01T00:00-00:45:00[-00:45]"; +result = action(relativeTo); +assert.sameValue(result, 366, "ISO string offset accepted with zero seconds (string)"); + +relativeTo = { year: 1970, month: 1, day: 1, offset: "+00:45:00.000000000", timeZone: "+00:45" }; +result = action(relativeTo); +assert.sameValue(result, 366, "ISO string offset accepted with zero seconds (property bag)"); + +relativeTo = "1970-01-01T00:00+00:44:30.123456789[+00:45]"; +assert.throws(RangeError, () => action(relativeTo), "rounding is not accepted between ISO offset and time zone"); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js new file mode 100644 index 0000000000..47a8fdb33f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-undefined-throw-on-calendar-units.js @@ -0,0 +1,35 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: > + The relativeTo option is required when the Duration contains years, months, + or weeks, and unit is days; or unit is weeks or months +features: [Temporal, arrow-function] +---*/ + +const oneYear = new Temporal.Duration(1); +const oneMonth = new Temporal.Duration(0, 1); +const oneWeek = new Temporal.Duration(0, 0, 1); +const oneDay = new Temporal.Duration(0, 0, 0, 1); + +const options = { unit: "days" }; +assert.sameValue(oneDay.total(options), 1, "days do not require relativeTo"); +assert.sameValue(oneDay.total("days"), 1, "days do not require relativeTo (string shorthand)"); +assert.throws(RangeError, () => oneWeek.total(options), "total days of weeks requires relativeTo"); +assert.throws(RangeError, () => oneWeek.total("days"), "total days of weeks requires relativeTo (string shorthand)"); +assert.throws(RangeError, () => oneMonth.total(options), "total days of months requires relativeTo"); +assert.throws(RangeError, () => oneMonth.total("days"), "total days of months requires relativeTo (string shorthand)"); +assert.throws(RangeError, () => oneYear.total(options), "total days of years requires relativeTo"); +assert.throws(RangeError, () => oneYear.total("days"), "total days of years requires relativeTo (string shorthand)"); + +["months", "weeks"].forEach((unit) => { + [oneDay, oneWeek, oneMonth, oneYear].forEach((duration) => { + assert.throws(RangeError, () => duration.total({ unit }), `${duration} total ${unit} requires relativeTo`); + assert.throws(RangeError, () => duration.total(unit), `${duration} total ${unit} requires relativeTo (string shorthand)`); + }); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js new file mode 100644 index 0000000000..1cb6e1bf8d --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-wrong-type.js @@ -0,0 +1,50 @@ +// |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.total +description: > + Appropriate error thrown when relativeTo cannot be converted to a valid + relativeTo string or property bag +features: [BigInt, Symbol, Temporal] +---*/ + +const timeZone = new Temporal.TimeZone('UTC'); +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +const primitiveTests = [ + [undefined, 'undefined'], + [null, 'null'], + [true, 'boolean'], + ['', 'empty string'], + [1, "number that doesn't convert to a valid ISO string"], + [1n, 'bigint'] +]; + +for (const [relativeTo, description] of primitiveTests) { + assert.throws( + typeof relativeTo === 'string' || typeof relativeTo === 'undefined' ? RangeError : TypeError, + () => instance.total({ unit: 'days', relativeTo }), + `${description} does not convert to a valid ISO string (first argument)` + ); +} + +const typeErrorTests = [ + [Symbol(), 'symbol'], + [{}, 'plain object'], + [Temporal.PlainDate, 'Temporal.PlainDate, object'], + [Temporal.PlainDate.prototype, 'Temporal.PlainDate.prototype, object'], + [Temporal.ZonedDateTime, 'Temporal.ZonedDateTime, object'], + [Temporal.ZonedDateTime.prototype, 'Temporal.ZonedDateTime.prototype, object'] +]; + +for (const [relativeTo, description] of typeErrorTests) { + assert.throws( + TypeError, + () => instance.total({ unit: 'days', relativeTo }), + `${description} is not a valid property bag and does not convert to a string` + ); +} + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js new file mode 100644 index 0000000000..a52a8c4943 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-negative-epochnanoseconds.js @@ -0,0 +1,26 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: A pre-epoch value is handled correctly by the modulo operation in GetISOPartsFromEpoch +info: | + sec-temporal-getisopartsfromepoch step 1: + 1. Let _remainderNs_ be the mathematical value whose sign is the sign of _epochNanoseconds_ and whose magnitude is abs(_epochNanoseconds_) modulo 10<sup>6</sup>. + sec-temporal-builtintimezonegetplaindatetimefor step 2: + 2. Let _result_ be ! GetISOPartsFromEpoch(_instant_.[[Nanoseconds]]). +features: [Temporal] +---*/ + +const relativeTo = new Temporal.ZonedDateTime(-13849764_999_999_999n, "UTC"); +const duration = new Temporal.Duration(0, 0, 0, 1); + +// This code path shows up anywhere we convert an exact time, before the Unix +// epoch, with nonzero microseconds or nanoseconds, into a wall time; in this +// case via relativeTo. + +const result = duration.total({ relativeTo, unit: "days" }); +assert.sameValue(result, 1); + +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 new file mode 100644 index 0000000000..d415117f89 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-normalized-time-duration-to-days-range-errors.js @@ -0,0 +1,128 @@ +// |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.total +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 oneNsDuration = Temporal.Duration.from({ nanoseconds: 1 }); +const negOneNsDuration = Temporal.Duration.from({ nanoseconds: -1 }); +const dayNs = 86_400_000_000_000; +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( + 0n, // Sets _startNs_ to 0 + timeZoneSubstituteValues( + [[epochInstant]], // Returned in step 16, setting _relativeResult_ + [ + TemporalHelpers.SUBSTITUTE_SKIP, // Pre-conversion in Duration.p.total + dayNs - 1, // Returned in step 8, setting _startDateTime_ + -dayNs + 1, // Returned in step 9, setting _endDateTime_ + ] + ) +); +assert.throws(RangeError, () => + // Using 1ns duration _nanoseconds_ to 1 and _sign_ to 1 + oneNsDuration.total({ + relativeTo: zdt, + unit: "day", + }), + "RangeError when days < 0 and sign = 1" +); + +// Step 23: days > 0 and sign = -1 +zdt = new Temporal.ZonedDateTime( + 0n, // Sets _startNs_ to 0 + timeZoneSubstituteValues( + [[epochInstant]], // Returned in step 16, setting _relativeResult_ + [ + TemporalHelpers.SUBSTITUTE_SKIP, // Pre-conversion in Duration.p.total + -dayNs + 1, // Returned in step 8, setting _startDateTime_ + dayNs - 1, // Returned in step 9, setting _endDateTime_ + ] + ) +); +assert.throws(RangeError, () => + // Using -1ns duration sets _nanoseconds_ to -1 and _sign_ to -1 + negOneNsDuration.total({ + relativeTo: zdt, + unit: "day", + }), + "RangeError when days > 0 and sign = -1" +); + +// Step 25: 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_ + ], + [ + TemporalHelpers.SUBSTITUTE_SKIP, // pre-conversion in Duration.p.total + dayNs - 1, // Returned in step 8, setting _startDateTime_ + -dayNs + 1, // Returned in step 9, setting _endDateTime_ + ] + ) +); +assert.throws(RangeError, () => + // Using -1ns duration sets _nanoseconds_ to -1 and _sign_ to -1 + negOneNsDuration.total({ + relativeTo: zdt, + unit: "day", + }), + "RangeError when nanoseconds > 0 and sign = -1" +); + +// Step 28: 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_ + [[new Temporal.Instant(2n ** 53n)]], + [] + ) +); +assert.throws(RangeError, () => + oneNsDuration.total({ + relativeTo: zdt, + unit: "days", + }), + "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/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js new file mode 100644 index 0000000000..4be071d057 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-non-integer.js @@ -0,0 +1,19 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: RangeError thrown if time zone reports an offset that is not an integer number of nanoseconds +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[3600_000_000_000.5, NaN, -Infinity, Infinity].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone); + assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: datetime })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js new file mode 100644 index 0000000000..0534adf10f --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-not-callable.js @@ -0,0 +1,23 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: TypeError thrown if timeZone.getOffsetNanosecondsFor is not callable +features: [BigInt, Symbol, Temporal, arrow-function] +---*/ + +[undefined, null, true, Math.PI, 'string', Symbol('sym'), 42n, {}].forEach((notCallable) => { + const timeZone = new Temporal.TimeZone("UTC"); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone); + timeZone.getOffsetNanosecondsFor = notCallable; + assert.throws( + TypeError, + () => duration.total({ unit: "seconds", relativeTo: datetime }), + `Uncallable ${notCallable === null ? 'null' : typeof notCallable} getOffsetNanosecondsFor should throw TypeError` + ); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js new file mode 100644 index 0000000000..10d39099e8 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-out-of-range.js @@ -0,0 +1,19 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: RangeError thrown if time zone reports an offset that is out of range +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[-86400_000_000_000, 86400_000_000_000].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone); + assert.throws(RangeError, () => duration.total({ unit: "seconds", relativeTo: datetime })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js new file mode 100644 index 0000000000..26cd533b6c --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-timezone-getoffsetnanosecondsfor-wrong-type.js @@ -0,0 +1,28 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: TypeError thrown if time zone reports an offset that is not a Number +features: [Temporal] +includes: [temporalHelpers.js] +---*/ + +[ + undefined, + null, + true, + "+01:00", + Symbol(), + 3600_000_000_000n, + {}, + { valueOf() { return 3600_000_000_000; } }, +].forEach((wrongOffset) => { + const timeZone = TemporalHelpers.specificOffsetTimeZone(wrongOffset); + const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); + const datetime = new Temporal.ZonedDateTime(1_000_000_000_987_654_321n, timeZone); + assert.throws(TypeError, () => duration.total({ unit: "seconds", relativeTo: datetime })); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js new file mode 100644 index 0000000000..8173ef0b23 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days-different-sign.js @@ -0,0 +1,38 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Relative to a ZonedDateTime with a fractional number of days and different sign. +features: [Temporal] +---*/ + +let duration = Temporal.Duration.from({ + weeks: 1, + days: 0, + hours: 1, +}); + +let cal = new class extends Temporal.Calendar { + #dateAdd = 0; + + dateAdd(date, duration, options) { + if (++this.#dateAdd === 1) { + duration = "-P1W"; + } + return super.dateAdd(date, duration, options); + } +}("iso8601"); + +let zdt = new Temporal.ZonedDateTime(0n, "UTC", cal); + +let result = duration.total({ + relativeTo: zdt, + unit: "days", +}); + +assert.sameValue(result, -7 + 1 / 24); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js new file mode 100644 index 0000000000..6f234590f0 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/relativeto-zoneddatetime-with-fractional-days.js @@ -0,0 +1,27 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + Relative to a ZonedDateTime with a fractional number of days. +features: [Temporal] +---*/ + +let duration = Temporal.Duration.from({ + weeks: 1, + days: 0, + hours: 1, +}); + +let zdt = new Temporal.ZonedDateTime(0n, "UTC", "iso8601"); + +let result = duration.total({ + relativeTo: zdt, + unit: "days", +}); + +assert.sameValue(result, 7 + 1 / 24); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js new file mode 100644 index 0000000000..eda1477282 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/shell.js @@ -0,0 +1,24 @@ +// GENERATED, DO NOT EDIT +// file: isConstructor.js +// Copyright (C) 2017 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +description: | + Test if a given function is a constructor function. +defines: [isConstructor] +features: [Reflect.construct] +---*/ + +function isConstructor(f) { + if (typeof f !== "function") { + throw new Test262Error("isConstructor invoked with a non-function value"); + } + + try { + Reflect.construct(function(){}, [], f); + } catch (e) { + return false; + } + return true; +} diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js new file mode 100644 index 0000000000..bd7ca83961 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/timezone-getpossibleinstantsfor-iterable.js @@ -0,0 +1,36 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: An iterable returned from timeZone.getPossibleInstantsFor is consumed after each call +info: | + sec-temporal.duration.prototype.total steps 4 and 10: + 4. Let _relativeTo_ be ? ToRelativeTemporalObject(_options_). + 10. Let _balanceResult_ be ? BalanceDuration(_unbalanceResult_.[[Days]], [...], _unbalanceResult_.[[Nanoseconds]], _unit_, _intermediate_). + sec-temporal-torelativetemporalobject step 6.d: + d. Let _epochNanoseconds_ be ? InterpretISODateTimeOffset(_result_.[[Year]], [...], _result_.[[Nanosecond]], _offsetNs_, _timeZone_, *"compatible"*, *"reject"*). + sec-temporal-interpretisodatetimeoffset step 7: + 7. Let _possibleInstants_ be ? GetPossibleInstantsFor(_timeZone_, _dateTime_). + sec-temporal-addzoneddatetime step 8: + 8. Let _intermediateInstant_ be ? BuiltinTimeZoneGetInstantFor(_timeZone_, _intermediateDateTime_, *"compatible"*). + sec-temporal-builtintimezonegetinstantfor step 1: + 1. Let _possibleInstants_ be ? GetPossibleInstantsFor(_timeZone_, _dateTime_). + sec-temporal-getpossibleinstantsfor step 2: + 2. Let _list_ be ? IterableToList(_possibleInstants_). +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const expected = [ + "2000-01-01T00:00:00", // called once on the input relativeTo if ZonedDateTime + "2001-02-09T00:00:00", // called once on the intermediate ZonedDateTime with the calendar parts of the Duration added +]; + +TemporalHelpers.checkTimeZonePossibleInstantsIterable((timeZone) => { + const duration = new Temporal.Duration(1, 1, 1, 1, 1, 1, 1); + duration.total({ unit: 'seconds', relativeTo: { year: 2000, month: 1, day: 1, timeZone } }); +}, expected); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js new file mode 100644 index 0000000000..ac37e1be6e --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-disallowed-units-string.js @@ -0,0 +1,29 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Specifically disallowed units for the unit option +features: [Temporal, arrow-function] +---*/ + +const instance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321); +const invalidUnits = [ + "era", + "eras", +]; +invalidUnits.forEach((unit) => { + assert.throws( + RangeError, + () => instance.total({ unit }), + `{ unit: "${unit}" } should not be allowed as an argument to total` + ); + assert.throws( + RangeError, + () => instance.total(unit), + `"${unit}" should not be allowed as an argument to total` + ); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js new file mode 100644 index 0000000000..119d7a5f05 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-invalid-string.js @@ -0,0 +1,21 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.protoype.total +description: RangeError thrown when unit option not one of the allowed string values +info: | + sec-getoption step 10: + 10. If _values_ is not *undefined* and _values_ does not contain an element equal to _value_, throw a *RangeError* exception. + sec-temporal-totemporaldurationtotal step 1: + 1. Let _unit_ be ? GetOption(_normalizedOptions_, *"unit"*, « String », « *"year"*, *"years"*, *"month"*, *"months"*, *"week"*, *"weeks"*, *"day"*, *"days"*, *"hour"*, *"hours"*, *"minute"*, *"minutes"*, *"second"*, *"seconds"*, *"millisecond"*, *"milliseconds"*, *"microsecond"*, *"microseconds"*, *"nanosecond"*, *"nanoseconds"* », *undefined*). + sec-temporal.duration.protoype.total step 5: + 5. Let _unit_ be ? ToTemporalDurationTotalUnit(_options_). +features: [Temporal] +---*/ + +const duration = new Temporal.Duration(0, 0, 0, 1); +assert.throws(RangeError, () => duration.total({ unit: "other string" })); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js new file mode 100644 index 0000000000..82b7932d1d --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted-string.js @@ -0,0 +1,24 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Plural units are accepted as well for the shorthand for the unit option +includes: [temporalHelpers.js] +features: [Temporal, arrow-function] +---*/ + +const duration = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321); +const validUnits = [ + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", +]; +TemporalHelpers.checkPluralUnitsAccepted((unit) => duration.total(unit), validUnits); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js new file mode 100644 index 0000000000..17737506cc --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-plurals-accepted.js @@ -0,0 +1,28 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Plural units are accepted as well for the unit option +includes: [temporalHelpers.js] +features: [Temporal] +---*/ + +const duration = new Temporal.Duration(1, 2, 3, 4, 5, 6, 7, 987, 654, 321); +const relativeTo = new Temporal.PlainDate(2000, 1, 1); +const validUnits = [ + "year", + "month", + "week", + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", +]; +TemporalHelpers.checkPluralUnitsAccepted((unit) => duration.total({ unit, relativeTo }), validUnits); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js new file mode 100644 index 0000000000..9aab057420 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-string-shorthand-string.js @@ -0,0 +1,27 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: String as first argument is equivalent to options bag with unit option +features: [Temporal, arrow-function] +---*/ + +const instance = new Temporal.Duration(0, 0, 0, 4, 5, 6, 7, 987, 654, 321); +const validUnits = [ + "day", + "hour", + "minute", + "second", + "millisecond", + "microsecond", + "nanosecond", +]; +validUnits.forEach((unit) => { + const full = instance.total({ unit }); + const shorthand = instance.total(unit); + assert.sameValue(shorthand, full, `"${unit}" as first argument to total is equivalent to options bag`); +}); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js new file mode 100644 index 0000000000..7fa1a8ba6b --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/unit-wrong-type.js @@ -0,0 +1,25 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2021 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: Type conversions for unit option +info: | + sec-getoption step 9.a: + a. Set _value_ to ? ToString(_value_). + sec-temporal-totemporaldurationtotal step 1: + 1. Let _unit_ be ? GetOption(_normalizedOptions_, *"unit"*, « String », « *"year"*, *"years"*, *"month"*, *"months"*, *"week"*, *"weeks"*, *"day"*, *"days"*, *"hour"*, *"hours"*, *"minute"*, *"minutes"*, *"second"*, *"seconds"*, *"millisecond"*, *"milliseconds"*, *"microsecond"*, *"microseconds"*, *"nanosecond"*, *"nanoseconds"* », *undefined*). + sec-temporal.duration.protoype.total step 5: + 5. Let _unit_ be ? ToTemporalDurationTotalUnit(_options_). +includes: [compareArray.js, temporalHelpers.js] +features: [Temporal] +---*/ + +const duration = new Temporal.Duration(0, 0, 0, 1); +TemporalHelpers.checkStringOptionWrongType("unit", "hour", + (unit) => duration.total({ unit }), + (result, descr) => assert.sameValue(result, 24, descr), +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js new file mode 100644 index 0000000000..eb790cce0a --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/year-zero.js @@ -0,0 +1,20 @@ +// |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.total +description: Negative zero, as an extended year, is rejected +features: [Temporal, arrow-function] +---*/ + +const instance = new Temporal.Duration(1, 0, 0, 0, 24); + +let relativeTo = "-000000-11-04T00:00"; +assert.throws( + RangeError, + () => { instance.total({ unit: "days", relativeTo }); }, + "reject minus zero as extended year" +); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js new file mode 100644 index 0000000000..8b93da72eb --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-day-length.js @@ -0,0 +1,43 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: A malicious time zone resulting a day length of zero is handled correctly +info: | + Based on a test by André Bargull. + + RoundDuration step 6: + d. Let _result_ be ? NanosecondsToDays(_nanoseconds_, _intermediate_). + e. Set _days_ to _days_ + _result_.[[Days]] + _result_.[[Nanoseconds]] / _result_.[[DayLength]]. + + NanosecondsToDays steps 19-23: + 19. If _days_ < 0 and _sign_ = 1, throw a *RangeError* exception. + 20. If _days_ > 0 and _sign_ = -1, throw a *RangeError* exception. + 21. If _nanoseconds_ < 0, then + a. Assert: sign is -1. + 22. If _nanoseconds_ > 0 and _sign_ = -1, throw a *RangeError* exception. + 23. Assert: The inequality abs(_nanoseconds_) < abs(_dayLengthNs_) holds. +features: [Temporal] +---*/ + +const instance = new Temporal.Duration(0, 0, 0, 0, -24, 0, 0, 0, 0, -1); + +const tz = new class extends Temporal.TimeZone { + #getPossibleInstantsForCalls = 0; + + getPossibleInstantsFor(dt) { + this.#getPossibleInstantsForCalls++; + + if (this.#getPossibleInstantsForCalls <= 2) { + return [new Temporal.Instant(-86400_000_000_000n - 2n)] + } + return super.getPossibleInstantsFor(dt); + } +}("UTC"); + +const relativeTo = new Temporal.ZonedDateTime(0n, tz, "iso8601"); +assert.throws(RangeError, () => instance.total({ relativeTo, unit: "days" })); + +reportCompare(0, 0); diff --git a/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js new file mode 100644 index 0000000000..8036c28320 --- /dev/null +++ b/js/src/tests/test262/built-ins/Temporal/Duration/prototype/total/zero-year-month-week-length.js @@ -0,0 +1,34 @@ +// |reftest| skip-if(!this.hasOwnProperty('Temporal')) -- Temporal is not enabled unconditionally +// Copyright (C) 2023 Igalia, S.L. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.duration.prototype.total +description: > + A malicious calendar resulting in a year, month, or week length of zero is + handled correctly +info: | + RoundDuration + 10.z. If _oneYearDays_ = 0, throw a *RangeError* exception. + ... + 11.z. If _oneMonthDays_ = 0, throw a *RangeError* exception. + ... + 12.s. If _oneWeekDays_ = 0, throw a *RangeError* exception. +features: [Temporal] +---*/ + +const cal = new class extends Temporal.Calendar { + dateAdd(date, duration, options) { + // Called several times, last call sets oneYear/Month/WeekDays to 0 + return new Temporal.PlainDate(1970, 1, 1); + } +}("iso8601"); + +const instance = new Temporal.Duration(1, 0, 0, 0, 0, 0, 0, 0, 0, 1); +const relativeTo = new Temporal.ZonedDateTime(0n, "UTC", cal); + +assert.throws(RangeError, () => instance.total({ relativeTo, unit: "years" }), "zero year length handled correctly"); +assert.throws(RangeError, () => instance.total({ relativeTo, unit: "months" }), "zero month length handled correctly"); +assert.throws(RangeError, () => instance.total({ relativeTo, unit: "weeks" }), "zero week length handled correctly"); + +reportCompare(0, 0); |