Skip to content

Commit

Permalink
Add tests for precise results in Duration.p.total and ZonedDateTime.p…
Browse files Browse the repository at this point in the history
….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 tc39#3961, so the
new tests from this commit start at "precision-exact-mathematical-values-6.js".
  • Loading branch information
anba authored and ptomato committed Jan 22, 2024
1 parent 561962e commit 60538bc
Show file tree
Hide file tree
Showing 3 changed files with 398 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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}`,
);
}
Original file line number Diff line number Diff line change
@@ -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}`,
);
}
}
}
Loading

0 comments on commit 60538bc

Please sign in to comment.