Skip to content

Commit

Permalink
Normative: Limit time portion of durations to <2⁵³ seconds
Browse files Browse the repository at this point in the history
In order to avoid unbounded integer arithmetic, we place an upper bound on
the total of the time portion of a duration (days through nanoseconds).

For the purpose of determining the limits, days are always 24 hours, even
if a calendar day may be a different number of hours relative to a
particular ZonedDateTime.

It's now no longer possible to make Duration.prototype.total() come up
with an infinite result; removing the loops in UnbalanceDurationRelative
made it so that the duration's calendar units must be able to be added to
a relativeTo date without overflowing, and this change makes it so that
the duration's time units are too small to overflow to infinity either.
  • Loading branch information
ptomato committed Nov 6, 2023
1 parent 2afc671 commit 4805af1
Show file tree
Hide file tree
Showing 4 changed files with 41 additions and 172 deletions.
15 changes: 4 additions & 11 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,6 @@ export class Duration {
calendarRec
));
// If the unit we're totalling is smaller than `days`, convert days down to that unit.
let balanceResult;
if (zonedRelativeTo) {
const intermediate = ES.MoveRelativeZonedDateTime(
zonedRelativeTo,
Expand All @@ -527,7 +526,7 @@ export class Duration {
0,
precalculatedPlainDateTime
);
balanceResult = ES.BalancePossiblyInfiniteTimeDurationRelative(
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDurationRelative(
days,
hours,
minutes,
Expand All @@ -538,9 +537,9 @@ export class Duration {
unit,
intermediate,
timeZoneRec
);
));
} else {
balanceResult = ES.BalancePossiblyInfiniteTimeDuration(
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDuration(
days,
hours,
minutes,
Expand All @@ -549,14 +548,8 @@ export class Duration {
microseconds,
nanoseconds,
unit
);
}
if (balanceResult === 'positive overflow') {
return Infinity;
} else if (balanceResult === 'negative overflow') {
return -Infinity;
));
}
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = balanceResult);
// Finally, truncate to the correct unit and calculate remainder
const { total } = ES.RoundDuration(
years,
Expand Down
109 changes: 23 additions & 86 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const MathSign = Math.sign;
const MathTrunc = Math.trunc;
const NumberIsFinite = Number.isFinite;
const NumberIsNaN = Number.isNaN;
const NumberIsSafeInteger = Number.isSafeInteger;
const NumberMaxSafeInteger = Number.MAX_SAFE_INTEGER;
const ObjectCreate = Object.create;
const ObjectDefineProperty = Object.defineProperty;
Expand Down Expand Up @@ -3309,11 +3310,10 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre
// back inside the period where it belongs. Note that this case only can
// happen for positive durations because the only direction that
// `disambiguation: 'compatible'` can change clock time is forwards.
days = bigInt(days);
if (sign === 1) {
while (days.greater(0) && relativeResult.epochNs.greater(endNs)) {
days = days.prev();
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days.toJSNumber());
while (days > 0 && relativeResult.epochNs.greater(endNs)) {
days--;
relativeResult = AddDaysToZonedDateTime(start, dtStart, timeZoneRec, calendar, days);
// may do disambiguation
}
}
Expand All @@ -3336,10 +3336,10 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre
if (isOverflow) {
nanoseconds = nanoseconds.subtract(dayLengthNs);
relativeResult = oneDayFarther;
days = days.add(sign);
days += sign;
}
} while (isOverflow);
if (!days.isZero() && MathSign(days.toJSNumber()) != sign) {
if (days !== 0 && MathSign(days) != sign) {
throw new RangeError('Time zone or calendar converted nanoseconds into a number of days with the opposite sign');
}
if (!nanoseconds.isZero() && MathSign(nanoseconds.toJSNumber()) != sign) {
Expand All @@ -3351,7 +3351,7 @@ export function NanosecondsToDays(nanoseconds, zonedRelativeTo, timeZoneRec, pre
if (nanoseconds.abs().geq(MathAbs(dayLengthNs))) {
throw new Error('assert not reached');
}
return { days: days.toJSNumber(), nanoseconds, dayLengthNs: MathAbs(dayLengthNs) };
return { days, nanoseconds, dayLengthNs: MathAbs(dayLengthNs) };
}

export function BalanceTimeDuration(
Expand All @@ -3363,33 +3363,6 @@ export function BalanceTimeDuration(
microseconds,
nanoseconds,
largestUnit
) {
let result = BalancePossiblyInfiniteTimeDuration(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
largestUnit
);
if (result === 'positive overflow' || result === 'negative overflow') {
throw new RangeError('Duration out of range');
} else {
return result;
}
}

export function BalancePossiblyInfiniteTimeDuration(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
largestUnit
) {
hours = bigInt(hours).add(bigInt(days).multiply(24));
nanoseconds = TotalDurationNanoseconds(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
Expand Down Expand Up @@ -3449,21 +3422,7 @@ export function BalancePossiblyInfiniteTimeDuration(
microseconds = microseconds.toJSNumber() * sign;
nanoseconds = nanoseconds.toJSNumber() * sign;

if (
!NumberIsFinite(days) ||
!NumberIsFinite(hours) ||
!NumberIsFinite(minutes) ||
!NumberIsFinite(seconds) ||
!NumberIsFinite(milliseconds) ||
!NumberIsFinite(microseconds) ||
!NumberIsFinite(nanoseconds)
) {
if (sign === 1) {
return 'positive overflow';
} else if (sign === -1) {
return 'negative overflow';
}
}
RejectDuration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
}

Expand All @@ -3479,38 +3438,6 @@ export function BalanceTimeDurationRelative(
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime
) {
let result = BalancePossiblyInfiniteTimeDurationRelative(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
largestUnit,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime
);
if (result === 'positive overflow' || result === 'negative overflow') {
throw new RangeError('Duration out of range');
}
return result;
}

export function BalancePossiblyInfiniteTimeDurationRelative(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
largestUnit,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime
) {
const startNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS);
const startInstant = GetSlot(zonedRelativeTo, INSTANT);
Expand Down Expand Up @@ -3541,11 +3468,17 @@ export function BalancePossiblyInfiniteTimeDurationRelative(
days = 0;
}

const result = BalancePossiblyInfiniteTimeDuration(0, 0, 0, 0, 0, 0, nanoseconds, largestUnit);
if (result === 'positive overflow' || result === 'negative overflow') {
return result;
}
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = result);
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
0,
0,
0,
0,
0,
0,
nanoseconds,
largestUnit
));

return { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds };
}

Expand Down Expand Up @@ -3755,6 +3688,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 (!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');
}
}

export function DifferenceISODate(y1, m1, d1, y2, m2, d2, largestUnit = 'days') {
Expand Down Expand Up @@ -5926,6 +5862,7 @@ export function RoundDuration(
break;
}
}
RejectDuration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
return { years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, total };
}

Expand Down
12 changes: 8 additions & 4 deletions polyfill/test/validStrings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -364,22 +364,26 @@ const durationHoursFraction = withCode(fraction, (data, result) => {
const digitsNotInfinite = withSyntaxConstraints(oneOrMore(digit()), (result) => {
if (!Number.isFinite(+result)) throw new SyntaxError('try again on infinity');
});
const timeDurationDigits = (factor) =>
withSyntaxConstraints(between(1, 16, digit()), (result) => {
if (!Number.isSafeInteger(+result * factor)) throw new SyntaxError('try again on unsafe integer');
});
const durationSeconds = seq(
withCode(digitsNotInfinite, (data, result) => (data.seconds = +result * data.factor)),
withCode(timeDurationDigits(1), (data, result) => (data.seconds = +result * data.factor)),
[durationSecondsFraction],
secondsDesignator
);
const durationMinutes = seq(
withCode(digitsNotInfinite, (data, result) => (data.minutes = +result * data.factor)),
withCode(timeDurationDigits(60), (data, result) => (data.minutes = +result * data.factor)),
choice(seq(minutesDesignator, [durationSeconds]), seq(durationMinutesFraction, minutesDesignator))
);
const durationHours = seq(
withCode(digitsNotInfinite, (data, result) => (data.hours = +result * data.factor)),
withCode(timeDurationDigits(3600), (data, result) => (data.hours = +result * data.factor)),
choice(seq(hoursDesignator, [choice(durationMinutes, durationSeconds)]), seq(durationHoursFraction, hoursDesignator))
);
const durationTime = seq(timeDesignator, choice(durationHours, durationMinutes, durationSeconds));
const durationDays = seq(
withCode(digitsNotInfinite, (data, result) => (data.days = +result * data.factor)),
withCode(timeDurationDigits(86400), (data, result) => (data.days = +result * data.factor)),
daysDesignator
);
const durationWeeks = seq(
Expand Down
77 changes: 6 additions & 71 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -535,12 +535,9 @@ <h1>Temporal.Duration.prototype.total ( _totalOf_ )</h1>
1. Let _unbalanceResult_ be ? UnbalanceDateDurationRelative(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _unit_, _plainRelativeTo_, _calendarRec_).
1. If _zonedRelativeTo_ is not *undefined*, then
1. Let _intermediate_ be ? MoveRelativeZonedDateTime(_zonedRelativeTo_, _calendarRec_, _timeZoneRec_, _unbalanceResult_.[[Years]], _unbalanceResult_.[[Months]], _unbalanceResult_.[[Weeks]], 0, _precalculatedPlainDateTime_).
1. Let _balanceResult_ be ? BalancePossiblyInfiniteTimeDurationRelative(_unbalanceResult_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _unit_, _intermediate_, _timeZoneRec_).
1. Let _balanceResult_ be ? BalanceTimeDurationRelative(_unbalanceResult_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _unit_, _intermediate_, _timeZoneRec_).
1. Else,
1. Let _balanceResult_ be BalancePossiblyInfiniteTimeDuration(_unbalanceResult_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _unit_).
1. If _balanceResult_ is ~positive overflow~, return *+∞*<sub>𝔽</sub>.
1. If _balanceResult_ is ~negative overflow~, return *-∞*<sub>𝔽</sub>.
1. Assert: _balanceResult_ is a Time Duration Record.
1. Let _balanceResult_ be ! BalanceTimeDuration(_unbalanceResult_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _unit_).
1. Let _roundRecord_ be ? RoundDuration(_unbalanceResult_.[[Years]], _unbalanceResult_.[[Months]], _unbalanceResult_.[[Weeks]], _balanceResult_.[[Days]], _balanceResult_.[[Hours]], _balanceResult_.[[Minutes]], _balanceResult_.[[Seconds]], _balanceResult_.[[Milliseconds]], _balanceResult_.[[Microseconds]], _balanceResult_.[[Nanoseconds]], 1, _unit_, *"trunc"*, _plainRelativeTo_, _calendarRec_, _zonedRelativeTo_, _timeZoneRec_, _precalculatedPlainDateTime_).
1. Return 𝔽(_roundRecord_.[[Total]]).
</emu-alg>
Expand Down Expand Up @@ -1060,6 +1057,8 @@ <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. 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*.
</emu-alg>
</emu-clause>
Expand Down Expand Up @@ -1232,32 +1231,6 @@ <h1>
<dt>description</dt>
<dd>It converts the time units of a duration into a form where lower units are converted into higher units as much as possible, up to _largestUnit_. If the Number value for any unit is infinite, it returns abruptly with a *RangeError*.</dd>
</dl>
<emu-alg>
1. Let _balanceResult_ be BalancePossiblyInfiniteTimeDuration(_days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _largestUnit_).
1. If _balanceResult_ is ~positive overflow~ or ~negative overflow~, then
1. Throw a *RangeError* exception.
1. Else,
1. Return _balanceResult_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-balancepossiblyinfinitetimeduration" type="abstract operation">
<h1>
BalancePossiblyInfiniteTimeDuration (
_days_: an integer,
_hours_: an integer,
_minutes_: an integer,
_seconds_: an integer,
_milliseconds_: an integer,
_microseconds_: an integer,
_nanoseconds_: an integer,
_largestUnit_: a String,
): a Time Duration Record if there are no infinite values, or either ~positive overflow~ or ~negative overflow~ in case of infinite values
</h1>
<dl class="header">
<dt>description</dt>
<dd>It converts the time units of a duration into a form where lower units are converted into higher units as much as possible, up to _largestUnit_. If the Number value for any unit is infinite, it returns a special value indicating the direction of overflow.</dd>
</dl>
<emu-alg>
1. Set _hours_ to _hours_ + _days_ &times; 24.
1. Set _nanoseconds_ to TotalDurationNanoseconds(_hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_).
Expand Down Expand Up @@ -1314,47 +1287,13 @@ <h1>
1. Set _nanoseconds_ to _nanoseconds_ modulo 1000.
1. Else,
1. Assert: _largestUnit_ is *"nanosecond"*.
1. For each value _v_ of « _days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_ », do
1. If 𝔽(_v_) is not finite, then
1. If _sign_ = 1, then
1. Return ~positive overflow~.
1. Else if _sign_ = -1, then
1. Return ~negative overflow~.
1. Return ! CreateTimeDurationRecord(_days_ &times; _sign_, _hours_ &times; _sign_, _minutes_ &times; _sign_, _seconds_ &times; _sign_, _milliseconds_ &times; _sign_, _microseconds_ &times; _sign_, _nanoseconds_ &times; _sign_).
1. Return ? CreateTimeDurationRecord(_days_ &times; _sign_, _hours_ &times; _sign_, _minutes_ &times; _sign_, _seconds_ &times; _sign_, _milliseconds_ &times; _sign_, _microseconds_ &times; _sign_, _nanoseconds_ &times; _sign_).
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-balancetimedurationrelative" type="abstract operation">
<h1>
BalanceTimeDurationRelative (
_days_: an integer,
_hours_: an integer,
_minutes_: an integer,
_seconds_: an integer,
_milliseconds_: an integer,
_microseconds_: an integer,
_nanoseconds_: an integer,
_largestUnit_: a String,
_zonedRelativeTo_: a Temporal.ZonedDateTime,
_timeZoneRec_: a Time Zone Methods Record,
_precalculatedPlainDateTime_: a Temporal.PlainDateTime or *undefined*,
): either a normal completion containing a Time Duration Record, or an abrupt completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>It converts the time units of a duration into a form where lower units are converted into higher units as much as possible, up to _largestUnit_, taking day length of _zonedRelativeTo_ into account. If the Number value for any unit is infinite, it returns abruptly with a *RangeError*.</dd>
</dl>
<emu-alg>
1. Let _balanceResult_ be ? BalancePossiblyInfiniteTimeDurationRelative(_days_, _hours_, _minutes_, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _largestUnit_, _zonedRelativeTo_, _timeZoneRec_, _precalculatedPlainDateTime_).
1. If _balanceResult_ is ~positive overflow~ or ~negative overflow~, then
1. Throw a *RangeError* exception.
1. Return _balanceResult_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-temporal-balancepossiblyinfinitetimedurationrelative" type="abstract operation">
<h1>
BalancePossiblyInfiniteTimeDurationRelative (
_days_: an integer,
_hours_: an integer,
_minutes_: an integer,
Expand Down Expand Up @@ -1393,11 +1332,7 @@ <h1>
1. Set _largestUnit_ to *"hour"*.
1. Else,
1. Set _days_ to 0.
1. If 𝔽(_days_) is not finite, then
1. If _days_ &gt; 0, return ~positive overflow~.
1. Else, return ~negative overflow~.
1. Let _balanceResult_ be BalancePossiblyInfiniteTimeDuration(0, 0, 0, 0, 0, 0, _result_.[[Nanoseconds]], _largestUnit_).
1. If _balanceResult_ is ~positive overflow~ or ~negative overflow~, return _balanceResult_.
1. Let _balanceResult_ be ? BalanceTimeDuration(0, 0, 0, 0, 0, 0, _result_.[[Nanoseconds]], _largestUnit_).
1. Return ! CreateTimeDurationRecord(_days_, _balanceResult_.[[Hours]], _balanceResult_.[[Minutes]], _balanceResult_.[[Seconds]], _balanceResult_.[[Milliseconds]], _balanceResult_.[[Microseconds]], _balanceResult_.[[Nanoseconds]]).
</emu-alg>
</emu-clause>
Expand Down

0 comments on commit 4805af1

Please sign in to comment.