From 0620aed009764505638b469b55b491d03276bbe0 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 410dcc1dcd..f2224acb3e 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -3663,6 +3663,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³²'); + } if (!NumberIsSafeInteger(d * 86400 + h * 3600 + min * 60 + s + MathTrunc(ms / 1e3 + µs / 1e6 + ns / 1e9))) { throw new RangeError('total of duration time units cannot exceed 9007199254740991.999999999 s'); } @@ -5032,6 +5035,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); @@ -5345,20 +5391,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; @@ -5402,12 +5438,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; @@ -5441,23 +5473,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 1cb2fcc8d3..bd1ea046b6 100644 --- a/spec/duration.html +++ b/spec/duration.html @@ -1196,6 +1196,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. If abs(_normalizedSeconds_) ≥ 253, return *false*. 1. Return *true*.