Skip to content

Commit

Permalink
Normative: Limit years, months, and weeks to <2³² each
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ptomato committed Jun 20, 2023
1 parent 14b39fa commit 4df75fe
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 30 deletions.
84 changes: 54 additions & 30 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3546,6 +3546,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');
}
Expand Down Expand Up @@ -4911,6 +4914,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, 86400e9);
const wholeDays = epochNs.minus(remainder);
Expand Down Expand Up @@ -5212,19 +5258,9 @@ 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);
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 / oneYearDays + norm.div(oneYearDays * dayLengthNs);
years = RoundJSNumberToIncrement(total, increment, roundingMode);
months = weeks = days = 0;
norm = TimeDuration.ZERO;
break;
Expand Down Expand Up @@ -5267,12 +5303,8 @@ export function RoundDuration(
let { days: oneMonthDays } = MoveRelativeDate(calendarRec, plainRelativeTo, oneMonth);

oneMonthDays = MathAbs(oneMonthDays);
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 / oneMonthDays + norm.div(oneMonthDays * dayLengthNs);
months = RoundJSNumberToIncrement(total, increment, roundingMode);
weeks = days = 0;
norm = TimeDuration.ZERO;
break;
Expand Down Expand Up @@ -5305,23 +5337,15 @@ export function RoundDuration(
let { days: oneWeekDays } = MoveRelativeDate(calendarRec, plainRelativeTo, oneWeek);

oneWeekDays = MathAbs(oneWeekDays);
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 / oneWeekDays + norm.div(oneWeekDays * dayLengthNs);
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.div(dayLengthNs);
days = RoundJSNumberToIncrement(total, increment, roundingMode);
norm = TimeDuration.ZERO;
break;
}
Expand Down
3 changes: 3 additions & 0 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,9 @@ <h1>
1. If 𝔽(_v_) is not finite, return *false*.
1. If _v_ &lt; 0 and _sign_ &gt; 0, return *false*.
1. If _v_ &gt; 0 and _sign_ &lt; 0, return *false*.
1. If abs(_years_) &ge; 2<sup>32</sup>, return *false*.
1. If abs(_months_) &ge; 2<sup>32</sup>, return *false*.
1. If abs(_weeks_) &ge; 2<sup>32</sup>, return *false*.
1. Let _normalizedSeconds_ be _days_ &times; 86,400 + _hours_ &times; 3600 + _minutes_ &times; 60 + _seconds_ + _milliseconds_ &times; 10<sup>-3</sup> + _microseconds_ &times; 10<sup>-6</sup> + _nanoseconds_ &times; 10<sup>-9</sup>.
1. If abs(_normalizedSeconds_) &ge; 2<sup>53</sup>, return *false*.
1. Return *true*.
Expand Down

0 comments on commit 4df75fe

Please sign in to comment.