From be83c499575d6cc7d43b8c436a4f62e3173c796c Mon Sep 17 00:00:00 2001 From: Philip Chimento Date: Mon, 19 Jun 2023 18:30:40 +0200 Subject: [PATCH] =?UTF-8?q?Normative:=20Limit=20duration=20years,=20months?= =?UTF-8?q?,=20and=20weeks=20to=20<2=C2=B3=C2=B2=20each?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order to prevent having to use bigint arithmetic, limit years, months, and weeks to 32 bits each in durations. There are more changes to the reference code than to the spec in this commit because the upper limit now allows us to rewrite the reference code's RoundDuration algorithm in a way that's more similar to how it was already specified in the spec text. --- polyfill/lib/ecmascript.mjs | 84 ++++++++++++++++++++++------------ polyfill/test/validStrings.mjs | 10 ++-- spec/duration.html | 3 ++ 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 1588450b5f..805bb4e190 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -3664,6 +3664,9 @@ export function RejectDuration(y, mon, w, d, h, min, s, ms, µs, ns) { const propSign = MathSign(prop); if (propSign !== 0 && propSign !== sign) throw new RangeError('mixed-sign values not allowed as duration fields'); } + if (MathAbs(y) >= 2 ** 32 || MathAbs(mon) >= 2 ** 32 || MathAbs(w) >= 2 ** 32) { + throw new RangeError('years, months, and weeks must be < 2³²'); + } const msResult = TruncatingDivModByPowerOf10(ms, 3); const µsResult = TruncatingDivModByPowerOf10(µs, 6); const nsResult = TruncatingDivModByPowerOf10(ns, 9); @@ -5047,6 +5050,49 @@ export function RoundNumberToIncrement(quantity, increment, mode) { return quotient.multiply(increment); } +export function RoundJSNumberToIncrement(quantity, increment, mode) { + let quotient = MathTrunc(quantity / increment); + const remainder = quantity % increment; + if (remainder === 0) return quantity; + const sign = remainder < 0 ? -1 : 1; + const tiebreaker = MathAbs(remainder * 2); + const tie = tiebreaker === increment; + const expandIsNearer = tiebreaker > increment; + switch (mode) { + case 'ceil': + if (sign > 0) quotient += sign; + break; + case 'floor': + if (sign < 0) quotient += sign; + break; + case 'expand': + // always expand if there is a remainder + quotient += sign; + break; + case 'trunc': + // no change needed, because divmod is a truncation + break; + case 'halfCeil': + if (expandIsNearer || (tie && sign > 0)) quotient += sign; + break; + case 'halfFloor': + if (expandIsNearer || (tie && sign < 0)) quotient += sign; + break; + case 'halfExpand': + // "half up away from zero" + if (expandIsNearer || tie) quotient += sign; + break; + case 'halfTrunc': + if (expandIsNearer) quotient += sign; + break; + case 'halfEven': { + if (expandIsNearer || (tie && quotient % 2 === 1)) quotient += sign; + break; + } + } + return quotient * increment; +} + export function RoundInstant(epochNs, increment, unit, roundingMode) { let { remainder } = NonNegativeBigIntDivmod(epochNs, DAY_NANOS); const wholeDays = epochNs.minus(remainder); @@ -5361,20 +5407,10 @@ export function RoundDuration( const oneYear = new TemporalDuration(days < 0 ? -1 : 1); let { days: oneYearDays } = MoveRelativeDate(calendarRec, plainRelativeTo, oneYear); - // Note that `nanoseconds` below (here and in similar code for months, - // weeks, and days further below) isn't actually nanoseconds for the - // full date range. Instead, it's a BigInt representation of total - // days multiplied by the number of nanoseconds in the last day of - // the duration. This lets us do days-or-larger rounding using BigInt - // math which reduces precision loss. oneYearDays = MathAbs(oneYearDays); if (oneYearDays === 0) throw new RangeError('custom calendar reported that a year is 0 days long'); - const divisor = bigInt(oneYearDays).multiply(dayLengthNs); - const nanoseconds = divisor.multiply(years).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); - const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment).toJSNumber(), roundingMode); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - years = rounded.divide(divisor).toJSNumber(); + total = years + (days + norm.fdiv(dayLengthNs)) / oneYearDays; + years = RoundJSNumberToIncrement(total, increment, roundingMode); months = weeks = days = 0; norm = TimeDuration.ZERO; break; @@ -5418,12 +5454,8 @@ export function RoundDuration( oneMonthDays = MathAbs(oneMonthDays); if (oneMonthDays === 0) throw new RangeError('custom calendar reported that a month is 0 days long'); - const divisor = bigInt(oneMonthDays).multiply(dayLengthNs); - const nanoseconds = divisor.multiply(months).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); - const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - months = rounded.divide(divisor).toJSNumber(); + total = months + (days + norm.fdiv(dayLengthNs)) / oneMonthDays; + months = RoundJSNumberToIncrement(total, increment, roundingMode); weeks = days = 0; norm = TimeDuration.ZERO; break; @@ -5457,23 +5489,15 @@ export function RoundDuration( oneWeekDays = MathAbs(oneWeekDays); if (oneWeekDays === 0) throw new RangeError('custom calendar reported that a week is 0 days long'); - const divisor = bigInt(oneWeekDays).multiply(dayLengthNs); - const nanoseconds = divisor.multiply(weeks).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs); - const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - weeks = rounded.divide(divisor).toJSNumber(); + total = weeks + (days + norm.fdiv(dayLengthNs)) / oneWeekDays; + weeks = RoundJSNumberToIncrement(total, increment, roundingMode); days = 0; norm = TimeDuration.ZERO; break; } case 'day': { - const divisor = bigInt(dayLengthNs); - const nanoseconds = divisor.multiply(days).plus(norm.totalNs); - const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode); - const { quotient, remainder } = nanoseconds.divmod(divisor); - total = quotient.toJSNumber() + remainder.toJSNumber() / divisor; - days = rounded.divide(divisor).toJSNumber(); + total = days + norm.fdiv(dayLengthNs); + days = RoundJSNumberToIncrement(total, increment, roundingMode); norm = TimeDuration.ZERO; break; } diff --git a/polyfill/test/validStrings.mjs b/polyfill/test/validStrings.mjs index b14564f643..7613e1ebd5 100644 --- a/polyfill/test/validStrings.mjs +++ b/polyfill/test/validStrings.mjs @@ -361,8 +361,8 @@ const durationHoursFraction = withCode(fraction, (data, result) => { data.nanoseconds = Math.trunc(ns % 1e3) * data.factor; }); -const digitsNotInfinite = withSyntaxConstraints(oneOrMore(digit()), (result) => { - if (!Number.isFinite(+result)) throw new SyntaxError('try again on infinity'); +const uint32Digits = withSyntaxConstraints(between(1, 10, digit()), (result) => { + if (+result >= 2 ** 32) throw new SyntaxError('try again for an uint32'); }); const timeDurationDigits = (factor) => withSyntaxConstraints(between(1, 16, digit()), (result) => { @@ -387,17 +387,17 @@ const durationDays = seq( daysDesignator ); const durationWeeks = seq( - withCode(digitsNotInfinite, (data, result) => (data.weeks = +result * data.factor)), + withCode(uint32Digits, (data, result) => (data.weeks = +result * data.factor)), weeksDesignator, [durationDays] ); const durationMonths = seq( - withCode(digitsNotInfinite, (data, result) => (data.months = +result * data.factor)), + withCode(uint32Digits, (data, result) => (data.months = +result * data.factor)), monthsDesignator, [choice(durationWeeks, durationDays)] ); const durationYears = seq( - withCode(digitsNotInfinite, (data, result) => (data.years = +result * data.factor)), + withCode(uint32Digits, (data, result) => (data.years = +result * data.factor)), yearsDesignator, [choice(durationMonths, durationWeeks, durationDays)] ); diff --git a/spec/duration.html b/spec/duration.html index 9e10e1463c..40b0866d21 100644 --- a/spec/duration.html +++ b/spec/duration.html @@ -1203,6 +1203,9 @@

1. If 𝔽(_v_) is not finite, return *false*. 1. If _v_ < 0 and _sign_ > 0, return *false*. 1. If _v_ > 0 and _sign_ < 0, return *false*. + 1. If abs(_years_) ≥ 232, return *false*. + 1. If abs(_months_) ≥ 232, return *false*. + 1. If abs(_weeks_) ≥ 232, return *false*. 1. Let _normalizedSeconds_ be _days_ × 86,400 + _hours_ × 3600 + _minutes_ × 60 + _seconds_ + _milliseconds_ × 10-3 + _microseconds_ × 10-6 + _nanoseconds_ × 10-9. 1. NOTE: The above step cannot be implemented directly using floating-point arithmetic. Multiplying by 10-3, 10-6, and 10-9 respectively may be imprecise when _milliseconds_, _microseconds_, or _nanoseconds_ is an unsafe integer. This multiplication can be implemented in C++ with an implementation of `std::remquo()` with sufficient bits in the quotient. String manipulation will also give an exact result, since the multiplication is by a power of 10. 1. If abs(_normalizedSeconds_) ≥ 253, return *false*.