diff --git a/test/intl402/DurationFormat/prototype/format/duration-out-of-range-1.js b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-1.js new file mode 100644 index 00000000000..e96c327210a --- /dev/null +++ b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-1.js @@ -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}` + ); + } +} diff --git a/test/intl402/DurationFormat/prototype/format/duration-out-of-range-2.js b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-2.js new file mode 100644 index 00000000000..4b533e9369f --- /dev/null +++ b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-2.js @@ -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}` + ); + } +} diff --git a/test/intl402/DurationFormat/prototype/format/duration-out-of-range-3.js b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-3.js new file mode 100644 index 00000000000..4c5cc0fc5bc --- /dev/null +++ b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-3.js @@ -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` + ); + } +} diff --git a/test/intl402/DurationFormat/prototype/format/duration-out-of-range-4.js b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-4.js new file mode 100644 index 00000000000..80ee3782e89 --- /dev/null +++ b/test/intl402/DurationFormat/prototype/format/duration-out-of-range-4.js @@ -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` +); diff --git a/test/intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js b/test/intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js index 8ea4fd1144c..8de16322e6c 100644 --- a/test/intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js +++ b/test/intl402/DurationFormat/prototype/format/precision-exact-mathematical-values.js @@ -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, }, ];