From e292fb80dec61a1443babfcd1211965941cd50a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Tue, 5 Jul 2022 12:48:26 +0200 Subject: [PATCH] Import SpiderMonkey Temporal tests Temporal tests written for the SpiderMonkey implementation. Mostly covers edge cases around mathematical operations and regression tests for reported spec bugs. --- ...s-precision-exact-mathematical-values-1.js | 77 +++++++++++ ...s-precision-exact-mathematical-values-2.js | 45 ++++++ .../precision-exact-mathematical-values-1.js | 96 +++++++++++++ .../precision-exact-mathematical-values-2.js | 98 +++++++++++++ .../precision-exact-mathematical-values.js | 130 ++++++++++++++++++ 5 files changed, 446 insertions(+) create mode 100644 test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-1.js create mode 100644 test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-2.js create mode 100644 test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js create mode 100644 test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js create mode 100644 test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values.js diff --git a/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-1.js b/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-1.js new file mode 100644 index 0000000000..286e1033f1 --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-1.js @@ -0,0 +1,77 @@ +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.zoneddatetime.prototype.round +description: > + NanosecondsToDays computes with precise mathematical integers. +info: | + NanosecondsToDays ( nanoseconds, relativeTo ) + ... + 14. If sign is 1, then + a. Repeat, while days > 0 and intermediateNs > endNs, + i. Set days to days - 1. + ii. ... + + Ensure |days = days - 1| is exact and doesn't loose precision. +features: [Temporal] +---*/ + +var expectedDurationDays = [ + Number.MAX_SAFE_INTEGER + 4, // 9007199254740996 + Number.MAX_SAFE_INTEGER + 3, // 9007199254740994 + Number.MAX_SAFE_INTEGER + 2, // 9007199254740992 + Number.MAX_SAFE_INTEGER + 1, // 9007199254740992 + Number.MAX_SAFE_INTEGER + 0, // 9007199254740991 + Number.MAX_SAFE_INTEGER - 1, // 9007199254740990 + Number.MAX_SAFE_INTEGER - 2, // 9007199254740989 + Number.MAX_SAFE_INTEGER - 3, // 9007199254740988 + Number.MAX_SAFE_INTEGER - 4, // 9007199254740987 + Number.MAX_SAFE_INTEGER - 5, // 9007199254740986 +]; + +// Intentionally not Test262Error to ensure assertions errors are propagated. +class StopExecution extends Error {} + +var cal = new class extends Temporal.Calendar { + #dateUntil = 0; + + dateUntil(one, two, options) { + if (++this.#dateUntil === 1) { + return Temporal.Duration.from({days: Number.MAX_SAFE_INTEGER + 4}); + } + return super.dateUntil(one, two, options); + } + + #dateAdd = 0; + + dateAdd(date, duration, options) { + // Ensure we don't add too many days which would lead to creating an invalid date. + if (++this.#dateAdd === 3) { + assert.sameValue(duration.days, Number.MAX_SAFE_INTEGER + 4); + + // The added days must be larger than 5 for the |intermediateNs > endNs| condition. + return super.dateAdd(date, "P6D", options); + } + + // Ensure the duration days are exact. + if (this.#dateAdd > 3) { + if (!expectedDurationDays.length) { + throw new StopExecution(); + } + assert.sameValue(duration.days, expectedDurationDays.shift()); + + // Add more than 5 for the |intermediateNs > endNs| condition. + return super.dateAdd(date, "P6D", options); + } + + // Otherwise call the default implementation. + return super.dateAdd(date, duration, options); + } +}("iso8601"); + +var zoned = new Temporal.ZonedDateTime(0n, "UTC", cal); +var duration = Temporal.Duration.from({days: 5}); +var options = {smallestUnit: "days", relativeTo: zoned}; + +assert.throws(StopExecution, () => duration.round(options)); diff --git a/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-2.js b/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-2.js new file mode 100644 index 0000000000..9f3975b3e0 --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/round/nanoseconds-to-days-precision-exact-mathematical-values-2.js @@ -0,0 +1,45 @@ +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-temporal.zoneddatetime.prototype.round +description: > + NanosecondsToDays computes with precise mathematical integers. +info: | + NanosecondsToDays ( nanoseconds, relativeTo ) + ... + 17. Repeat, while done is false, + ... + c. If (nanoseconds - dayLengthNs) × sign ≥ 0, then + ... + iii. Set days to days + sign. + + Ensure |days = days + sign| is exact and doesn't loose precision. +features: [Temporal] +---*/ + +var cal = new class extends Temporal.Calendar { + #dateUntil = 0; + + dateUntil(one, two, options) { + if (++this.#dateUntil === 1) { + return Temporal.Duration.from({days: Number.MAX_SAFE_INTEGER + 10}); + } + return super.dateUntil(one, two, options); + } + + #dateAdd = 0; + + dateAdd(date, duration, options) { + if (++this.#dateAdd === 3) { + return super.dateAdd(date, "P1D", options); + } + return super.dateAdd(date, duration, options); + } +}("iso8601"); + +var zoned = new Temporal.ZonedDateTime(0n, "UTC", cal); +var duration = Temporal.Duration.from({days: 5}); +var result = duration.round({smallestUnit: "days", relativeTo: zoned}); + +assert.sameValue(result.days, Number(BigInt(Number.MAX_SAFE_INTEGER + 10) + 5n)); diff --git a/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js new file mode 100644 index 0000000000..e2cc0073ea --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-1.js @@ -0,0 +1,96 @@ +// 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 next Number in direction -Infinity is less precise. +assert.sameValue(nextDown(expected), 4000); + +// The next Number in direction +Infinity is less precise. +assert.sameValue(nextUp(expected), 4000.000000000001); + +assert.sameValue(total, expected); diff --git a/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js new file mode 100644 index 0000000000..410ba056d5 --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-2.js @@ -0,0 +1,98 @@ +// 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); diff --git a/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values.js b/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values.js new file mode 100644 index 0000000000..52a6970cb0 --- /dev/null +++ b/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values.js @@ -0,0 +1,130 @@ +// Copyright (C) 2022 André Bargull. All rights reserved. +// This code is governed by the BSD license found in the LICENSE file. + +/*--- +esid: sec-get-temporal.zoneddatetime.prototype.hoursinday +description: > + Hours in day is correctly rounded using precise mathematical values. +info: | + get Temporal.ZonedDateTime.prototype.hoursInDay + + ... + 15. Let diffNs be tomorrowInstant.[[Nanoseconds]] - todayInstant.[[Nanoseconds]]. + 16. Return 𝔽(diffNs / (3.6 × 10^12)). +features: [Temporal] +---*/ + +const maxInstant = 86_40000_00000_00000_00000n; + +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]; +} + +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]; +} + +function BigIntAbs(n) { + return n >= 0 ? n : -n; +} + +function test(todayInstant, tomorrowInstant, expected) { + let timeZone = new class extends Temporal.TimeZone { + #getPossibleInstantsFor = 0; + + getPossibleInstantsFor() { + if (++this.#getPossibleInstantsFor === 1) { + return [new Temporal.Instant(todayInstant)]; + } + assert.sameValue(this.#getPossibleInstantsFor, 2); + return [new Temporal.Instant(tomorrowInstant)]; + } + }("UTC"); + + let zdt = new Temporal.ZonedDateTime(0n, timeZone); + let zdt_hoursInDay = zdt.hoursInDay; + + assert.sameValue(zdt_hoursInDay, expected); + + // Ensure the |expected| value is actually correctly rounded. + + const nsPerSec = 1000n * 1000n * 1000n; + const secPerHour = 60n * 60n; + const nsPerHour = secPerHour * nsPerSec; + + function toNanoseconds(hours) { + let wholeHours = BigInt(Math.trunc(hours)) * nsPerHour; + let fractionalHours = BigInt(Math.trunc(hours % 1 * Number(nsPerHour))); + return wholeHours + fractionalHours; + } + + let diff = tomorrowInstant - todayInstant; + let nanosInDay = toNanoseconds(zdt_hoursInDay); + + // The next number gives a less precise result. + let next = toNanoseconds(nextUp(zdt_hoursInDay)); + assert(BigIntAbs(diff - nanosInDay) <= BigIntAbs(diff - next)); + + // The previous number gives a less precise result. + let prev = toNanoseconds(nextDown(zdt_hoursInDay)); + assert(BigIntAbs(diff - nanosInDay) <= BigIntAbs(diff - prev)); + + // This computation can be inaccurate. + let inaccurate = Number(diff) / Number(nsPerHour); + + // Doing it component-wise can produce more accurate results. + let hours = Number(diff / nsPerSec) / Number(secPerHour); + let fractionalHours = Number(diff % nsPerSec) / Number(nsPerHour); + assert.sameValue(hours + fractionalHours, expected); + + // Ensure the result is more precise than the inaccurate result. + let inaccurateNanosInDay = toNanoseconds(inaccurate); + assert(BigIntAbs(diff - nanosInDay) <= BigIntAbs(diff - inaccurateNanosInDay)); +} + +test(-maxInstant, 0n, 2400000000); +test(-maxInstant, 1n, 2400000000); +test(-maxInstant, 10n, 2400000000); +test(-maxInstant, 100n, 2400000000); + +test(-maxInstant, 1_000n, 2400000000); +test(-maxInstant, 10_000n, 2400000000); +test(-maxInstant, 100_000n, 2400000000); + +test(-maxInstant, 1_000_000n, 2400000000.0000005); +test(-maxInstant, 10_000_000n, 2400000000.000003); +test(-maxInstant, 100_000_000n, 2400000000.0000277); + +test(-maxInstant, 1_000_000_000n, 2400000000.000278); +test(-maxInstant, 10_000_000_000n, 2400000000.0027776); +test(-maxInstant, 100_000_000_000n, 2400000000.0277777); + +test(-maxInstant, maxInstant, 4800000000); +test(-maxInstant, maxInstant - 10_000_000_000n, 4799999999.997222); +test(-maxInstant, maxInstant - 10_000_000_000n + 1n, 4799999999.997222); +test(-maxInstant, maxInstant - 10_000_000_000n - 1n, 4799999999.997222); + +test(maxInstant, -maxInstant, -4800000000); +test(maxInstant, -(maxInstant - 10_000_000_000n), -4799999999.997222); +test(maxInstant, -(maxInstant - 10_000_000_000n + 1n), -4799999999.997222); +test(maxInstant, -(maxInstant - 10_000_000_000n - 1n), -4799999999.997222);