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*.