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".
This commit is contained in:
André Bargull 2024-01-02 13:43:59 +01:00 committed by Philip Chimento
parent 99d5bc8c1b
commit 0de91996e7
3 changed files with 398 additions and 0 deletions

View File

@ -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}`,
);
}

View File

@ -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}`,
);
}
}
}

View File

@ -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}`,
);
}