Update tests for "Limit valid values for DurationFormats to match upcoming limits in Temporal"

Update tests for
<https://github.com/tc39/proposal-intl-duration-format/pull/173>.
This commit is contained in:
André Bargull 2024-01-12 15:49:27 +01:00 committed by Philip Chimento
parent 0596ff6981
commit ab809f8f0c
5 changed files with 486 additions and 30 deletions

View File

@ -0,0 +1,84 @@
// Copyright (C) 2024 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-Intl.DurationFormat.prototype.format
description: >
IsValidDurationRecord rejects too large "years", "months", and "weeks" values.
info: |
Intl.DurationFormat.prototype.format ( duration )
...
3. Let record be ? ToDurationRecord(duration).
...
ToDurationRecord ( input )
...
24. If IsValidDurationRecord(result) is false, throw a RangeError exception.
...
IsValidDurationRecord ( record )
...
6. If abs(years) 2^32, return false.
7. If abs(months) 2^32, return false.
8. If abs(weeks) 2^32, return false.
...
features: [Intl.DurationFormat]
---*/
const df = new Intl.DurationFormat();
const units = [
"years",
"months",
"weeks",
];
const invalidValues = [
2**32,
2**32 + 1,
Number.MAX_SAFE_INTEGER,
Number.MAX_VALUE,
];
const validValues = [
2**32 - 1,
];
for (let unit of units) {
for (let value of invalidValues) {
let positive = {[unit]: value};
assert.throws(
RangeError,
() => df.format(positive),
`Duration "${unit}" throws when value is ${value}`
);
// Also test with flipped sign.
let negative = {[unit]: -value};
assert.throws(
RangeError,
() => df.format(negative),
`Duration "${unit}" throws when value is ${-value}`
);
}
for (let value of validValues) {
// We don't care about the exact contents of the returned string, the call
// just shouldn't throw an exception.
let positive = {[unit]: value};
assert.sameValue(
typeof df.format(positive),
"string",
`Duration "${unit}" doesn't throw when value is ${value}`
);
// Also test with flipped sign.
let negative = {[unit]: -value};
assert.sameValue(
typeof df.format(negative),
"string",
`Duration "${unit}" doesn't throw when value is ${-value}`
);
}
}

View File

@ -0,0 +1,138 @@
// Copyright (C) 2024 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-Intl.DurationFormat.prototype.format
description: >
IsValidDurationRecord rejects too large "days", "hours", ... values.
info: |
Intl.DurationFormat.prototype.format ( duration )
...
3. Let record be ? ToDurationRecord(duration).
...
ToDurationRecord ( input )
...
24. If IsValidDurationRecord(result) is false, throw a RangeError exception.
...
IsValidDurationRecord ( record )
...
16. Let normalizedSeconds be days × 86,400 + hours × 3600 + minutes × 60 + seconds +
milliseconds × 10^-3 + microseconds × 10^-6 + nanoseconds × 10^-9.
17. If abs(normalizedSeconds) 2^53, return false.
...
features: [Intl.DurationFormat]
---*/
// 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;
}
let f64 = new Float64Array([num]);
let u64 = new BigUint64Array(f64.buffer);
u64[0] += (num < 0 ? 1n : -1n);
return f64[0];
}
const df = new Intl.DurationFormat();
const invalidValues = {
days: [
Math.ceil((Number.MAX_SAFE_INTEGER + 1) / 86400),
],
hours: [
Math.ceil((Number.MAX_SAFE_INTEGER + 1) / 3600),
],
minutes: [
Math.ceil((Number.MAX_SAFE_INTEGER + 1) / 60),
],
seconds: [
Number.MAX_SAFE_INTEGER + 1,
],
milliseconds: [
(Number.MAX_SAFE_INTEGER + 1) * 1e3,
9007199254740992_000,
],
microseconds: [
(Number.MAX_SAFE_INTEGER + 1) * 1e6,
9007199254740992_000_000,
],
nanoseconds: [
(Number.MAX_SAFE_INTEGER + 1) * 1e9,
9007199254740992_000_000_000,
],
};
const validValues = {
days: [
Math.floor(Number.MAX_SAFE_INTEGER / 86400),
],
hours: [
Math.floor(Number.MAX_SAFE_INTEGER / 3600),
],
minutes: [
Math.floor(Number.MAX_SAFE_INTEGER / 60),
],
seconds: [
Number.MAX_SAFE_INTEGER,
],
milliseconds: [
Number.MAX_SAFE_INTEGER * 1e3,
nextDown(9007199254740992_000),
],
microseconds: [
Number.MAX_SAFE_INTEGER * 1e6,
nextDown(9007199254740992_000_000),
],
nanoseconds: [
Number.MAX_SAFE_INTEGER * 1e9,
nextDown(9007199254740992_000_000_000),
],
};
for (let [unit, values] of Object.entries(invalidValues)) {
for (let value of values) {
let positive = {[unit]: value};
assert.throws(
RangeError,
() => df.format(positive),
`Duration "${unit}" throws when value is ${value}`
);
// Also test with flipped sign.
let negative = {[unit]: -value};
assert.throws(
RangeError,
() => df.format(negative),
`Duration "${unit}" throws when value is ${-value}`
);
}
}
for (let [unit, values] of Object.entries(validValues)) {
for (let value of values) {
// We don't care about the exact contents of the returned string, the call
// just shouldn't throw an exception.
let positive = {[unit]: value};
assert.sameValue(
typeof df.format(positive),
"string",
`Duration "${unit}" doesn't throw when value is ${value}`
);
// Also test with flipped sign.
let negative = {[unit]: -value};
assert.sameValue(
typeof df.format(negative),
"string",
`Duration "${unit}" doesn't throw when value is ${-value}`
);
}
}

View File

@ -0,0 +1,194 @@
// Copyright (C) 2024 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-Intl.DurationFormat.prototype.format
description: >
IsValidDurationRecord rejects too large time duration units.
info: |
Intl.DurationFormat.prototype.format ( duration )
...
3. Let record be ? ToDurationRecord(duration).
...
ToDurationRecord ( input )
...
24. If IsValidDurationRecord(result) is false, throw a RangeError exception.
...
IsValidDurationRecord ( record )
...
16. Let normalizedSeconds be days × 86,400 + hours × 3600 + minutes × 60 + seconds +
milliseconds × 10^-3 + microseconds × 10^-6 + nanoseconds × 10^-9.
17. If abs(normalizedSeconds) 2^53, return false.
...
features: [Intl.DurationFormat]
---*/
// 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];
}
// Negate |duration| similar to Temporal.Duration.prototype.negated.
function negatedDuration(duration) {
let result = {...duration};
for (let key of Object.keys(result)) {
// Add +0 to normalize -0 to +0.
result[key] = -result[key] + 0;
}
return result;
}
function fromNanoseconds(unit, value) {
switch (unit) {
case "days":
return value / (86400n * 1_000_000_000n);
case "hours":
return value / (3600n * 1_000_000_000n);
case "minutes":
return value / (60n * 1_000_000_000n);
case "seconds":
return value / 1_000_000_000n;
case "milliseconds":
return value / 1_000_000n;
case "microseconds":
return value / 1_000n;
case "nanoseconds":
return value;
}
throw new Error("invalid unit:" + unit);
}
function toNanoseconds(unit, value) {
switch (unit) {
case "days":
return value * 86400n * 1_000_000_000n;
case "hours":
return value * 3600n * 1_000_000_000n;
case "minutes":
return value * 60n * 1_000_000_000n;
case "seconds":
return value * 1_000_000_000n;
case "milliseconds":
return value * 1_000_000n;
case "microseconds":
return value * 1_000n;
case "nanoseconds":
return value;
}
throw new Error("invalid unit:" + unit);
}
const df = new Intl.DurationFormat();
const units = [
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
"microseconds",
"nanoseconds",
];
const zeroDuration = {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
microseconds: 0,
nanoseconds: 0,
};
const maxTimeDuration = BigInt(Number.MAX_SAFE_INTEGER) * 1_000_000_000n + 999_999_999n;
// Iterate over all time duration units and create the largest possible duration.
for (let i = 0; i < units.length; ++i) {
let unit = units[i];
// Test not only the next smallest unit, but all smaller units.
for (let j = i + 1; j < units.length; ++j) {
// Maximum duration value for |unit|.
let maxUnit = fromNanoseconds(unit, maxTimeDuration);
// Adjust |maxUnit| when the value is too large for Number.
let adjusted = BigInt(Number(maxUnit));
if (adjusted <= maxUnit) {
maxUnit = adjusted;
} else {
maxUnit = BigInt(nextDown(Number(maxUnit)));
}
// Remaining number of nanoseconds.
let remaining = maxTimeDuration - toNanoseconds(unit, maxUnit);
// Create the maximum valid duration.
let maxDuration = {
...zeroDuration,
[unit]: Number(maxUnit),
};
for (let k = j; k < units.length; ++k) {
let smallerUnit = units[k];
// Remaining number of nanoseconds in |smallerUnit|.
let remainingSmallerUnit = fromNanoseconds(smallerUnit, remaining);
maxDuration[smallerUnit] = Number(remainingSmallerUnit);
remaining -= toNanoseconds(smallerUnit, remainingSmallerUnit);
}
assert.sameValue(remaining, 0n, "zero remaining nanoseconds");
// We don't care about the exact contents of the returned string, the call
// just shouldn't throw an exception.
assert.sameValue(
typeof df.format(maxDuration),
"string",
`Duration "${JSON.stringify(maxDuration)}" doesn't throw`
);
// Also test with flipped sign.
let minDuration = negatedDuration(maxDuration);
// We don't care about the exact contents of the returned string, the call
// just shouldn't throw an exception.
assert.sameValue(
typeof df.format(minDuration),
"string",
`Duration "${JSON.stringify(minDuration)}" doesn't throw`
);
// Adding a single nanoseconds creates a too large duration.
let tooLargeDuration = {
...maxDuration,
nanoseconds: maxDuration.nanoseconds + 1,
};
assert.throws(
RangeError,
() => df.format(tooLargeDuration),
`Duration "${JSON.stringify(tooLargeDuration)}" throws`
);
// Also test with flipped sign.
let tooSmallDuration = negatedDuration(tooLargeDuration);
assert.throws(
RangeError,
() => df.format(tooSmallDuration),
`Duration "${JSON.stringify(tooSmallDuration)}" throws`
);
}
}

View File

@ -0,0 +1,63 @@
// Copyright (C) 2024 André Bargull. All rights reserved.
// This code is governed by the BSD license found in the LICENSE file.
/*---
esid: sec-Intl.DurationFormat.prototype.format
description: >
IsValidDurationRecord rejects too large time duration units.
info: |
Intl.DurationFormat.prototype.format ( duration )
...
3. Let record be ? ToDurationRecord(duration).
...
ToDurationRecord ( input )
...
24. If IsValidDurationRecord(result) is false, throw a RangeError exception.
...
IsValidDurationRecord ( record )
...
16. Let normalizedSeconds be days × 86,400 + hours × 3600 + minutes × 60 + seconds +
milliseconds × 10^-3 + microseconds × 10^-6 + nanoseconds × 10^-9.
17. If abs(normalizedSeconds) 2^53, return false.
...
features: [Intl.DurationFormat]
---*/
const df = new Intl.DurationFormat();
const duration = {
// Actual value is: 4503599627370497024
milliseconds: 4503599627370497_000,
// Actual value is: 4503599627370494951424
microseconds: 4503599627370495_000000,
};
// The naive approach to compute the duration seconds leads to an incorrect result.
let durationSecondsNaive = Math.trunc(duration.milliseconds / 1e3 + duration.microseconds / 1e6);
assert.sameValue(
Number.isSafeInteger(durationSecondsNaive),
false,
"Naive approach incorrectly computes duration seconds as out-of-range"
);
// The exact approach to compute the duration seconds leads to the correct result.
let durationSecondsExact = Number(BigInt(duration.milliseconds) / 1_000n) +
Number(BigInt(duration.microseconds) / 1_000_000n) +
Math.trunc(((duration.milliseconds % 1e3) * 1e3 + (duration.microseconds % 1e6)) / 1e6);
assert.sameValue(
Number.isSafeInteger(Number(durationSecondsExact)),
true,
"Exact approach correctly computes duration seconds as in-range"
);
// We don't care about the exact contents of the returned string, the call
// just shouldn't throw an exception.
assert.sameValue(
typeof df.format(duration),
"string",
`Duration "${JSON.stringify(duration)}" doesn't throw`
);

View File

@ -37,21 +37,6 @@ const durations = [
nanoseconds: 1,
},
// 9007199254740991 + (9007199254740991 / 10^3) + (9007199254740991 / 10^6) + (9007199254740991 / 10^9)
// = 9.016215470202185986731991 × 10^15
{
seconds: Number.MAX_SAFE_INTEGER,
milliseconds: Number.MAX_SAFE_INTEGER,
microseconds: Number.MAX_SAFE_INTEGER,
nanoseconds: Number.MAX_SAFE_INTEGER,
},
{
seconds: Number.MIN_SAFE_INTEGER,
milliseconds: Number.MIN_SAFE_INTEGER,
microseconds: Number.MIN_SAFE_INTEGER,
nanoseconds: Number.MIN_SAFE_INTEGER,
},
// 1 + (2 / 10^3) + (3 / 10^6) + (9007199254740991 / 10^9)
// = 9.007200256743991 × 10^6
{
@ -61,23 +46,15 @@ const durations = [
nanoseconds: Number.MAX_SAFE_INTEGER,
},
// 9007199254740991 + (10^3 / 10^3) + (10^6 / 10^6) + (10^9 / 10^9)
// = 9007199254740991 + 3
// = 9007199254740994
// (4503599627370497024 / 10^3) + (4503599627370494951424 / 10^6)
// = 4503599627370497.024 + 4503599627370494.951424
// = 9007199254740991.975424
{
seconds: Number.MAX_SAFE_INTEGER,
milliseconds: 10 ** 3,
microseconds: 10 ** 6,
nanoseconds: 10 ** 9,
},
// Actual value is: 4503599627370497024
milliseconds: 4503599627370497_000,
// ~1.7976931348623157e+308 / 10^9
// = ~1.7976931348623157 × 10^299
{
seconds: 0,
milliseconds: 0,
microseconds: 0,
nanoseconds: Number.MAX_VALUE,
// Actual value is: 4503599627370494951424
microseconds: 4503599627370495_000000,
},
];