Skip to content

Commit

Permalink
Editorial: Separate validation of roundingIncrement option
Browse files Browse the repository at this point in the history
Previously, the abstract operation ToTemporalRoundingIncrement was
technically unimplementable because of trying to calculate ℝ(infinity).

Address this by separating out the case where there is no maximum for
roundingIncrement into a separate code path from the case where there is a
maximum (which goes into a separate abstract operation,
ValidateTemporalRoundingIncrement.)

The separation is a bit awkward; it would be better if the truncated
integer was returned from ToTemporalRoundingIncrement, instead of the
original Number value, but that's not possible without a normative change
because currently, the range check is done with the original Number value.
(This is inconsistent with fractionalSecondDigits, where the range check
is done with the truncated integer.) I'm planning to make this normative
change as part of #2254 since we are changing the order of observable
property accesses on options bags anyway.

Closes: #2226
  • Loading branch information
ptomato committed Nov 30, 2022
1 parent caa941d commit 712c449
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 40 deletions.
51 changes: 31 additions & 20 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -772,12 +772,23 @@ export const ES = ObjectAssign({}, ES2022, {
ToShowOffsetOption: (options) => {
return ES.GetOption(options, 'offset', ['auto', 'never'], 'auto');
},
ToTemporalRoundingIncrement: (options, dividend, inclusive) => {
let maximum = Infinity;
if (dividend !== undefined) maximum = dividend;
if (!inclusive && dividend !== undefined) maximum = dividend > 1 ? dividend - 1 : 1;
const increment = ES.GetNumberOption(options, 'roundingIncrement', 1, maximum, 1);
if (dividend !== undefined && dividend % increment !== 0) {
ToTemporalRoundingIncrement: (options) => {
let increment = options.roundingIncrement;
if (increment === undefined) return 1;
increment = ES.ToNumber(increment);
if (!NumberIsFinite(increment) || increment < 1) {
throw new RangeError(`roundingIncrement must be at least 1 and finite, not ${increment}`);
}
return increment;
},
ValidateTemporalRoundingIncrement: (increment, dividend, inclusive) => {
let maximum = dividend;
if (!inclusive) maximum = dividend > 1 ? dividend - 1 : 1;
if (increment > maximum) {
throw new RangeError(`roundingIncrement must be at least 1 and less than ${maximum}, not ${increment}`);
}
increment = MathFloor(increment);
if (dividend % increment !== 0) {
throw new RangeError(`Rounding increment must divide evenly into ${dividend}`);
}
return increment;
Expand All @@ -795,7 +806,10 @@ export const ES = ObjectAssign({}, ES2022, {
microsecond: 1000,
nanosecond: 1000
};
return ES.ToTemporalRoundingIncrement(options, maximumIncrements[smallestUnit], false);
const increment = ES.ToTemporalRoundingIncrement(options);
const maximum = maximumIncrements[smallestUnit];
if (maximum == undefined) return MathFloor(increment);
return ES.ValidateTemporalRoundingIncrement(increment, maximum, false);
},
ToSecondsStringPrecision: (options) => {
const smallestUnit = ES.GetTemporalUnit(options, 'smallestUnit', 'time', undefined);
Expand Down Expand Up @@ -3553,7 +3567,12 @@ export const ES = ObjectAssign({}, ES2022, {
microsecond: 1000,
nanosecond: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, MAX_DIFFERENCE_INCREMENTS[smallestUnit], false);
let roundingIncrement = ES.ToTemporalRoundingIncrement(options);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(
roundingIncrement,
MAX_DIFFERENCE_INCREMENTS[smallestUnit],
false
);
const onens = GetSlot(first, EPOCHNANOSECONDS);
const twons = GetSlot(second, EPOCHNANOSECONDS);
let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceInstant(
Expand Down Expand Up @@ -3588,7 +3607,7 @@ export const ES = ObjectAssign({}, ES2022, {
}
let roundingMode = ES.ToTemporalRoundingMode(options, 'trunc');
if (operation === 'since') roundingMode = ES.NegateTemporalRoundingMode(roundingMode);
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, undefined, false);
const roundingIncrement = MathFloor(ES.ToTemporalRoundingIncrement(options));

const untilOptions = ObjectCreate(null);
ES.CopyDataProperties(untilOptions, options, []);
Expand Down Expand Up @@ -3727,7 +3746,8 @@ export const ES = ObjectAssign({}, ES2022, {
microsecond: 1000,
nanosecond: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, MAX_INCREMENTS[smallestUnit], false);
let roundingIncrement = ES.ToTemporalRoundingIncrement(options);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(roundingIncrement, MAX_INCREMENTS[smallestUnit], false);
let { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.DifferenceTime(
GetSlot(plainTime, ISO_HOUR),
GetSlot(plainTime, ISO_MINUTE),
Expand Down Expand Up @@ -3815,7 +3835,7 @@ export const ES = ObjectAssign({}, ES2022, {
}
let roundingMode = ES.ToTemporalRoundingMode(options, 'trunc');
if (operation === 'since') roundingMode = ES.NegateTemporalRoundingMode(roundingMode);
const roundingIncrement = ES.ToTemporalRoundingIncrement(options, undefined, false);
const roundingIncrement = MathFloor(ES.ToTemporalRoundingIncrement(options));

const fieldNames = ES.CalendarFields(calendar, ['monthCode', 'year']);
const otherFields = ES.PrepareTemporalFields(other, fieldNames, []);
Expand Down Expand Up @@ -5030,15 +5050,6 @@ export const ES = ObjectAssign({}, ES2022, {
}
return fallback;
},
GetNumberOption: (options, property, minimum, maximum, fallback) => {
let value = options[property];
if (value === undefined) return fallback;
value = ES.ToNumber(value);
if (NumberIsNaN(value) || value < minimum || value > maximum) {
throw new RangeError(`${property} must be between ${minimum} and ${maximum}, not ${value}`);
}
return MathFloor(value);
},
IsBuiltinCalendar: (id) => {
return ES.Call(ArrayIncludes, BUILTIN_CALENDAR_IDS, [ES.ASCIILowercase(id)]);
},
Expand Down
3 changes: 2 additions & 1 deletion polyfill/lib/instant.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export class Instant {
microsecond: 86400e6,
nanosecond: 86400e9
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo, maximumIncrements[smallestUnit], true);
let roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximumIncrements[smallestUnit], true);
const ns = GetSlot(this, EPOCHNANOSECONDS);
const roundedNs = ES.RoundInstant(ns, roundingIncrement, smallestUnit, roundingMode);
return new Instant(roundedNs);
Expand Down
3 changes: 2 additions & 1 deletion polyfill/lib/plaindatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ export class PlainDateTime {
microsecond: 1000,
nanosecond: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo, maximumIncrements[smallestUnit], false);
let roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximumIncrements[smallestUnit], false);

let year = GetSlot(this, ISO_YEAR);
let month = GetSlot(this, ISO_MONTH);
Expand Down
3 changes: 2 additions & 1 deletion polyfill/lib/plaintime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ export class PlainTime {
microsecond: 1000,
nanosecond: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo, MAX_INCREMENTS[smallestUnit], false);
let roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(roundingIncrement, MAX_INCREMENTS[smallestUnit], false);

let hour = GetSlot(this, ISO_HOUR);
let minute = GetSlot(this, ISO_MINUTE);
Expand Down
3 changes: 2 additions & 1 deletion polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,8 @@ export class ZonedDateTime {
microsecond: 1000,
nanosecond: 1000
};
const roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo, maximumIncrements[smallestUnit], false);
let roundingIncrement = ES.ToTemporalRoundingIncrement(roundTo);
roundingIncrement = ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximumIncrements[smallestUnit], false);

// first, round the underlying DateTime fields
const dt = dateTime(this);
Expand Down
48 changes: 35 additions & 13 deletions spec/abstractops.html
Original file line number Diff line number Diff line change
Expand Up @@ -196,31 +196,48 @@ <h1>ToShowOffsetOption ( _normalizedOptions_ )</h1>
<h1>
ToTemporalRoundingIncrement (
_normalizedOptions_: an Object,
_dividend_: an integer or *undefined*,
): either a normal completion containing a Number, or an abrupt completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It extracts the value of the property named *"roundingIncrement"* from _normalizedOptions_, makes sure it is a finite Number greater than or equal to 1, and returns that value.
It performs no further validation.
</dd>
</dl>
<emu-alg>
1. Let _increment_ be ? GetOption(_normalizedOptions_, *"roundingIncrement"*, *"number"*, *undefined*, *1*<sub>𝔽</sub>).
1. If _increment_ is not finite, throw a *RangeError* exception.
1. If _increment_ &lt; *1*<sub>𝔽</sub>, throw a *RangeError* exception.
1. Return _increment_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-validatetemporalroundingincrement" type="abstract operation">
<h1>
ValidateTemporalRoundingIncrement (
_increment_: a Number,
_dividend_: a positive integer,
_inclusive_: a Boolean,
)
): either a normal completion containing an integer, or an abrupt completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It extracts the value of the property named *"roundingIncrement"* from _normalizedOptions_, makes sure it is a valid value for the option, and returns that value rounded to an integer.
If _dividend_ is *undefined*, any non-zero positive value is valid.
Otherwise, the value must not be more than _dividend_ (or must be less than _dividend_, if _inclusive_ is *false*) and must divide evenly into _dividend_.
It truncates an _increment_ from ToTemporalRoundingIncrement to an integer and returns the result if it evenly divides _dividend_, otherwise throwing a *RangeError*.
_dividend_ must be divided into more than one part unless _inclusive_ is *true*.
</dd>
</dl>
<emu-alg>
1. If _dividend_ is *undefined*, then
1. Let _maximum_ be *+&infin;*<sub>𝔽</sub>.
1. Else if _inclusive_ is *true*, then
1. If _inclusive_ is *true*, then
1. Let _maximum_ be 𝔽(_dividend_).
1. Else if _dividend_ is more than 1, then
1. Let _maximum_ be 𝔽(_dividend_ - 1).
1. Else,
1. Let _maximum_ be *1*<sub>𝔽</sub>.
1. Let _increment_ be ? GetOption(_normalizedOptions_, *"roundingIncrement"*, *"number"*, *undefined*, *1*<sub>𝔽</sub>).
1. If _increment_ &lt; *1*<sub>𝔽</sub> or _increment_ &gt; _maximum_, throw a *RangeError* exception.
1. If _increment_ &gt; _maximum_, throw a *RangeError* exception.
1. Set _increment_ to floor(ℝ(_increment_)).
1. If _dividend_ is not *undefined* and _dividend_ modulo _increment_ is not zero, then
1. If _dividend_ modulo _increment_ is not zero, then
1. Throw a *RangeError* exception.
1. Return _increment_.
</emu-alg>
Expand All @@ -237,7 +254,8 @@ <h1>ToTemporalDateTimeRoundingIncrement ( _normalizedOptions_, _smallestUnit_ )<
1. Else,
1. Let _maximum_ be ! MaximumTemporalDurationRoundingIncrement(_smallestUnit_).
1. Assert: _maximum_ is not *undefined*.
1. Return ? ToTemporalRoundingIncrement(_normalizedOptions_, _maximum_, *false*).
1. Let _increment_ be ? ToTemporalRoundingIncrement(_normalizedOptions_).
1. Return ? ValidateTemporalRoundingIncrement(_increment_, _maximum_, *false*).
</emu-alg>
</emu-clause>

Expand Down Expand Up @@ -1794,7 +1812,11 @@ <h1>
1. If _operation_ is ~since~, then
1. Set _roundingMode_ to ! NegateTemporalRoundingMode(_roundingMode_).
1. Let _maximum_ be ! MaximumTemporalDurationRoundingIncrement(_smallestUnit_).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_options_, _maximum_, *false*).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_options_).
1. If _maximum_ is *undefined*, then
1. Set _roundingIncrement_ to floor(ℝ(_roundingIncrement_)).
1. Else,
1. Set _roundingIncrement_ to ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, *false*).
1. Return the Record {
[[SmallestUnit]]: _smallestUnit_,
[[LargestUnit]]: _largestUnit_,
Expand Down
6 changes: 5 additions & 1 deletion spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,11 @@ <h1>Temporal.Duration.prototype.round ( _roundTo_ )</h1>
1. If LargerOfTwoTemporalUnits(_largestUnit_, _smallestUnit_) is not _largestUnit_, throw a *RangeError* exception.
1. Let _roundingMode_ be ? ToTemporalRoundingMode(_roundTo_, *"halfExpand"*).
1. Let _maximum_ be ! MaximumTemporalDurationRoundingIncrement(_smallestUnit_).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_, _maximum_, *false*).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_).
1. If _maximum_ is *undefined*, then
1. Set _roundingIncrement_ to floor(ℝ(_roundingIncrement_)).
1. Else,
1. Set _roundingIncrement_ to ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, *false*).
1. Let _relativeTo_ be ? ToRelativeTemporalObject(_roundTo_).
1. Let _unbalanceResult_ be ? UnbalanceDurationRelative(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _largestUnit_, _relativeTo_).
1. Let _roundResult_ be (? RoundDuration(_unbalanceResult_.[[Years]], _unbalanceResult_.[[Months]], _unbalanceResult_.[[Weeks]], _unbalanceResult_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]], _roundingIncrement_, _smallestUnit_, _roundingMode_, _relativeTo_)).[[DurationRecord]].
Expand Down
3 changes: 2 additions & 1 deletion spec/instant.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,8 @@ <h1>Temporal.Instant.prototype.round ( _roundTo_ )</h1>
1. Else,
1. Assert: _smallestUnit_ is *"nanosecond"*.
1. Let _maximum_ be nsPerDay.
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_, _maximum_, *true*).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_).
1. Set _roundingIncrement_ to ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, *true*).
1. Let _roundedNs_ be ! RoundTemporalInstant(_instant_.[[Nanoseconds]], _roundingIncrement_, _smallestUnit_, _roundingMode_).
1. Return ! CreateTemporalInstant(_roundedNs_).
</emu-alg>
Expand Down
4 changes: 3 additions & 1 deletion spec/plaintime.html
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,9 @@ <h1>Temporal.PlainTime.prototype.round ( _roundTo_ )</h1>
1. Let _smallestUnit_ be ? GetTemporalUnit(_roundTo_, *"smallestUnit"*, ~time~, ~required~).
1. Let _roundingMode_ be ? ToTemporalRoundingMode(_roundTo_, *"halfExpand"*).
1. Let _maximum_ be ! MaximumTemporalDurationRoundingIncrement(_smallestUnit_).
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_, _maximum_, *false*).
1. Assert: _maximum_ is not *undefined*.
1. Let _roundingIncrement_ be ? ToTemporalRoundingIncrement(_roundTo_).
1. Set _roundingIncrement_ to ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, *false*).
1. Let _result_ be ! RoundTime(_temporalTime_.[[ISOHour]], _temporalTime_.[[ISOMinute]], _temporalTime_.[[ISOSecond]], _temporalTime_.[[ISOMillisecond]], _temporalTime_.[[ISOMicrosecond]], _temporalTime_.[[ISONanosecond]], _roundingIncrement_, _smallestUnit_, _roundingMode_).
1. Return ! CreateTemporalTime(_result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]]).
</emu-alg>
Expand Down

0 comments on commit 712c449

Please sign in to comment.