Skip to content

Commit 1104cad

Browse files
committed
Normative: Limit duration years, months, and weeks to <2³² each
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.
1 parent 156c764 commit 1104cad

File tree

3 files changed

+62
-35
lines changed

3 files changed

+62
-35
lines changed

polyfill/lib/ecmascript.mjs

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3665,6 +3665,9 @@ export function RejectDuration(y, mon, w, d, h, min, s, ms, µs, ns) {
36653665
const propSign = MathSign(prop);
36663666
if (propSign !== 0 && propSign !== sign) throw new RangeError('mixed-sign values not allowed as duration fields');
36673667
}
3668+
if (MathAbs(y) >= 2 ** 32 || MathAbs(mon) >= 2 ** 32 || MathAbs(w) >= 2 ** 32) {
3669+
throw new RangeError('years, months, and weeks must be < 2³²');
3670+
}
36683671
const msResult = TruncatingDivModByPowerOf10(ms, 3);
36693672
const µsResult = TruncatingDivModByPowerOf10(µs, 6);
36703673
const nsResult = TruncatingDivModByPowerOf10(ns, 9);
@@ -5014,6 +5017,49 @@ export function RoundNumberToIncrement(quantity, increment, mode) {
50145017
return quotient.multiply(increment);
50155018
}
50165019

5020+
export function RoundJSNumberToIncrement(quantity, increment, mode) {
5021+
let quotient = MathTrunc(quantity / increment);
5022+
const remainder = quantity % increment;
5023+
if (remainder === 0) return quantity;
5024+
const sign = remainder < 0 ? -1 : 1;
5025+
const tiebreaker = MathAbs(remainder * 2);
5026+
const tie = tiebreaker === increment;
5027+
const expandIsNearer = tiebreaker > increment;
5028+
switch (mode) {
5029+
case 'ceil':
5030+
if (sign > 0) quotient += sign;
5031+
break;
5032+
case 'floor':
5033+
if (sign < 0) quotient += sign;
5034+
break;
5035+
case 'expand':
5036+
// always expand if there is a remainder
5037+
quotient += sign;
5038+
break;
5039+
case 'trunc':
5040+
// no change needed, because divmod is a truncation
5041+
break;
5042+
case 'halfCeil':
5043+
if (expandIsNearer || (tie && sign > 0)) quotient += sign;
5044+
break;
5045+
case 'halfFloor':
5046+
if (expandIsNearer || (tie && sign < 0)) quotient += sign;
5047+
break;
5048+
case 'halfExpand':
5049+
// "half up away from zero"
5050+
if (expandIsNearer || tie) quotient += sign;
5051+
break;
5052+
case 'halfTrunc':
5053+
if (expandIsNearer) quotient += sign;
5054+
break;
5055+
case 'halfEven': {
5056+
if (expandIsNearer || (tie && quotient % 2 === 1)) quotient += sign;
5057+
break;
5058+
}
5059+
}
5060+
return quotient * increment;
5061+
}
5062+
50175063
export function RoundInstant(epochNs, increment, unit, roundingMode) {
50185064
let { remainder } = NonNegativeBigIntDivmod(epochNs, DAY_NANOS);
50195065
const wholeDays = epochNs.minus(remainder);
@@ -5328,20 +5374,10 @@ export function RoundDuration(
53285374
const oneYear = new TemporalDuration(days < 0 ? -1 : 1);
53295375
let { days: oneYearDays } = MoveRelativeDate(calendarRec, plainRelativeTo, oneYear);
53305376

5331-
// Note that `nanoseconds` below (here and in similar code for months,
5332-
// weeks, and days further below) isn't actually nanoseconds for the
5333-
// full date range. Instead, it's a BigInt representation of total
5334-
// days multiplied by the number of nanoseconds in the last day of
5335-
// the duration. This lets us do days-or-larger rounding using BigInt
5336-
// math which reduces precision loss.
53375377
oneYearDays = MathAbs(oneYearDays);
53385378
if (oneYearDays === 0) throw new RangeError('custom calendar reported that a year is 0 days long');
5339-
const divisor = bigInt(oneYearDays).multiply(dayLengthNs);
5340-
const nanoseconds = divisor.multiply(years).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs);
5341-
const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment).toJSNumber(), roundingMode);
5342-
const { quotient, remainder } = nanoseconds.divmod(divisor);
5343-
total = quotient.toJSNumber() + remainder.toJSNumber() / divisor;
5344-
years = rounded.divide(divisor).toJSNumber();
5379+
total = years + (days + norm.fdiv(dayLengthNs)) / oneYearDays;
5380+
years = RoundJSNumberToIncrement(total, increment, roundingMode);
53455381
months = weeks = days = 0;
53465382
norm = TimeDuration.ZERO;
53475383
break;
@@ -5385,12 +5421,8 @@ export function RoundDuration(
53855421

53865422
oneMonthDays = MathAbs(oneMonthDays);
53875423
if (oneMonthDays === 0) throw new RangeError('custom calendar reported that a month is 0 days long');
5388-
const divisor = bigInt(oneMonthDays).multiply(dayLengthNs);
5389-
const nanoseconds = divisor.multiply(months).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs);
5390-
const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode);
5391-
const { quotient, remainder } = nanoseconds.divmod(divisor);
5392-
total = quotient.toJSNumber() + remainder.toJSNumber() / divisor;
5393-
months = rounded.divide(divisor).toJSNumber();
5424+
total = months + (days + norm.fdiv(dayLengthNs)) / oneMonthDays;
5425+
months = RoundJSNumberToIncrement(total, increment, roundingMode);
53945426
weeks = days = 0;
53955427
norm = TimeDuration.ZERO;
53965428
break;
@@ -5424,23 +5456,15 @@ export function RoundDuration(
54245456

54255457
oneWeekDays = MathAbs(oneWeekDays);
54265458
if (oneWeekDays === 0) throw new RangeError('custom calendar reported that a week is 0 days long');
5427-
const divisor = bigInt(oneWeekDays).multiply(dayLengthNs);
5428-
const nanoseconds = divisor.multiply(weeks).plus(bigInt(days).multiply(dayLengthNs)).plus(norm.totalNs);
5429-
const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode);
5430-
const { quotient, remainder } = nanoseconds.divmod(divisor);
5431-
total = quotient.toJSNumber() + remainder.toJSNumber() / divisor;
5432-
weeks = rounded.divide(divisor).toJSNumber();
5459+
total = weeks + (days + norm.fdiv(dayLengthNs)) / oneWeekDays;
5460+
weeks = RoundJSNumberToIncrement(total, increment, roundingMode);
54335461
days = 0;
54345462
norm = TimeDuration.ZERO;
54355463
break;
54365464
}
54375465
case 'day': {
5438-
const divisor = bigInt(dayLengthNs);
5439-
const nanoseconds = divisor.multiply(days).plus(norm.totalNs);
5440-
const rounded = RoundNumberToIncrement(nanoseconds, divisor.multiply(increment), roundingMode);
5441-
const { quotient, remainder } = nanoseconds.divmod(divisor);
5442-
total = quotient.toJSNumber() + remainder.toJSNumber() / divisor;
5443-
days = rounded.divide(divisor).toJSNumber();
5466+
total = days + norm.fdiv(dayLengthNs);
5467+
days = RoundJSNumberToIncrement(total, increment, roundingMode);
54445468
norm = TimeDuration.ZERO;
54455469
break;
54465470
}

polyfill/test/validStrings.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ const durationHoursFraction = withCode(fraction, (data, result) => {
361361
data.nanoseconds = Math.trunc(ns % 1e3) * data.factor;
362362
});
363363

364-
const digitsNotInfinite = withSyntaxConstraints(oneOrMore(digit()), (result) => {
365-
if (!Number.isFinite(+result)) throw new SyntaxError('try again on infinity');
364+
const uint32Digits = withSyntaxConstraints(between(1, 10, digit()), (result) => {
365+
if (+result >= 2 ** 32) throw new SyntaxError('try again for an uint32');
366366
});
367367
const timeDurationDigits = (factor) =>
368368
withSyntaxConstraints(between(1, 16, digit()), (result) => {
@@ -387,17 +387,17 @@ const durationDays = seq(
387387
daysDesignator
388388
);
389389
const durationWeeks = seq(
390-
withCode(digitsNotInfinite, (data, result) => (data.weeks = +result * data.factor)),
390+
withCode(uint32Digits, (data, result) => (data.weeks = +result * data.factor)),
391391
weeksDesignator,
392392
[durationDays]
393393
);
394394
const durationMonths = seq(
395-
withCode(digitsNotInfinite, (data, result) => (data.months = +result * data.factor)),
395+
withCode(uint32Digits, (data, result) => (data.months = +result * data.factor)),
396396
monthsDesignator,
397397
[choice(durationWeeks, durationDays)]
398398
);
399399
const durationYears = seq(
400-
withCode(digitsNotInfinite, (data, result) => (data.years = +result * data.factor)),
400+
withCode(uint32Digits, (data, result) => (data.years = +result * data.factor)),
401401
yearsDesignator,
402402
[choice(durationMonths, durationWeeks, durationDays)]
403403
);

spec/duration.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1203,6 +1203,9 @@ <h1>
12031203
1. If 𝔽(_v_) is not finite, return *false*.
12041204
1. If _v_ &lt; 0 and _sign_ &gt; 0, return *false*.
12051205
1. If _v_ &gt; 0 and _sign_ &lt; 0, return *false*.
1206+
1. If abs(_years_) &ge; 2<sup>32</sup>, return *false*.
1207+
1. If abs(_months_) &ge; 2<sup>32</sup>, return *false*.
1208+
1. If abs(_weeks_) &ge; 2<sup>32</sup>, return *false*.
12061209
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>.
12071210
1. NOTE: The above step cannot be implemented directly using floating-point arithmetic. Multiplying by 10<sup>-3</sup>, 10<sup>-6</sup>, and 10<sup>-9</sup> 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.
12081211
1. If abs(_normalizedSeconds_) &ge; 2<sup>53</sup>, return *false*.

0 commit comments

Comments
 (0)