From 0de91996e7473b36534c6818aef7d9409657fb08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bargull?= Date: Tue, 2 Jan 2024 13:43:59 +0100 Subject: [PATCH] Add tests for precise results in Duration.p.total and ZonedDateTime.p.hoursInDay The existing tests didn't cover some edge cases where implementations have to compute the exact result of `numerator / denominator`, where at least one of `numerator` and `denominator` can't be exactly represented by an IEEE-754 double precision floating point value. "precision-exact-mathematical-values-5.js" gets added in #3961, so the new tests from this commit start at "precision-exact-mathematical-values-6.js". --- .../precision-exact-mathematical-values-6.js | 140 ++++++++++++++++++ .../precision-exact-mathematical-values-7.js | 119 +++++++++++++++ .../precision-exact-mathematical-values-2.js | 139 +++++++++++++++++ 3 files changed, 398 insertions(+) create mode 100644 test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js create mode 100644 test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js create mode 100644 test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values-2.js diff --git a/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js new file mode 100644 index 0000000000..fa65d391cb --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-6.js @@ -0,0 +1,140 @@ +// 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}`, + ); +} diff --git a/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js new file mode 100644 index 0000000000..eb3fa692a8 --- /dev/null +++ b/test/built-ins/Temporal/Duration/prototype/total/precision-exact-mathematical-values-7.js @@ -0,0 +1,119 @@ +// 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}`, + ); + } + } +} diff --git a/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values-2.js b/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values-2.js new file mode 100644 index 0000000000..faec865e15 --- /dev/null +++ b/test/built-ins/Temporal/ZonedDateTime/prototype/hoursInDay/precision-exact-mathematical-values-2.js @@ -0,0 +1,139 @@ +// Copyright (C) 2024 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 + + ... + 14. Let diffNs be tomorrowInstant.[[Nanoseconds]] - todayInstant.[[Nanoseconds]]. + 15. Return 𝔽(diffNs / (3.6 × 10^12)). +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 todayInstant = 0n; + let tomorrowInstant = BigInt(hours) * BigInt(nsPerHour) + BigInt(nanoseconds); + + 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 actual = zdt.hoursInDay; + + assert.sameValue( + actual, + expected, + `hours=${hours}, nanoseconds=${nanoseconds}`, + ); +}