Skip to content

Commit

Permalink
Normative: Avoid calling user code in no-op round operations
Browse files Browse the repository at this point in the history
If the given rounding options specify a smallest unit of nanoseconds and a
rounding increment of 1, then the rounding operation is a no-op. In that
case, return a copy of the duration, without actually performing the
rounding operation. This applies to all round(), until(), and since()
methods.

In the case of Duration.p.round(), other conditions must be fulfilled as
well for the operation to be a no-op: no balancing must take place.

In the case of Instant.p.round(), PlainDate.p.since(),
PlainDate.p.until(), PlainDateTime.p.round(), PlainDateTime.p.since(),
PlainDateTime.p.until(), PlainTime.p.round(), PlainTime.p.since(),
and PlainTime.p.until(), the change is not observable, so implementors are
free to make this optimization anyway.

This optimization was already the case in PlainDate.p.since/until() and
PlainYearMonth.p.since/until(), so this makes the other since/until()
methods consistent with those.
  • Loading branch information
ptomato committed Sep 13, 2023
1 parent 97baad3 commit 51ea969
Show file tree
Hide file tree
Showing 9 changed files with 145 additions and 81 deletions.
20 changes: 18 additions & 2 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class Duration {
let microseconds = GetSlot(this, MICROSECONDS);
let nanoseconds = GetSlot(this, NANOSECONDS);

let defaultLargestUnit = ES.DefaultTemporalLargestUnit(
const existingLargestUnit = ES.DefaultTemporalLargestUnit(
years,
months,
weeks,
Expand Down Expand Up @@ -254,7 +254,7 @@ export class Duration {
smallestUnitPresent = false;
smallestUnit = 'nanosecond';
}
defaultLargestUnit = ES.LargerOfTwoTemporalUnits(defaultLargestUnit, smallestUnit);
const defaultLargestUnit = ES.LargerOfTwoTemporalUnits(existingLargestUnit, smallestUnit);
let largestUnitPresent = true;
if (!largestUnit) {
largestUnitPresent = false;
Expand All @@ -279,6 +279,22 @@ export class Duration {
const maximum = maximumIncrements[smallestUnit];
if (maximum !== undefined) ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximum, false);

const roundingGranularityIsNoop = smallestUnit === 'nanosecond' && roundingIncrement === 1;
const balancingRequested = largestUnit !== existingLargestUnit;
const calendarUnitsPresent = years !== 0 || months !== 0 || weeks !== 0;
const timeUnitsOverflowWillOccur =
minutes >= 60 || seconds >= 60 || milliseconds >= 1000 || microseconds >= 1000 || nanoseconds >= 1000;
const hoursToDaysConversionMayOccur = (days !== 0 && ES.IsTemporalZonedDateTime(relativeTo)) || hours >= 24;
if (
roundingGranularityIsNoop &&
!balancingRequested &&
!calendarUnitsPresent &&
!timeUnitsOverflowWillOccur &&
!hoursToDaysConversionMayOccur
) {
return new Duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}

({ years, months, weeks, days } = ES.UnbalanceDateDurationRelative(
years,
months,
Expand Down
159 changes: 84 additions & 75 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4045,21 +4045,23 @@ export function DifferenceInstant(ns1, ns2, increment, smallestUnit, largestUnit
let milliseconds = diff.divide(1e6).mod(1e3).toJSNumber();
let seconds = diff.divide(1e9).toJSNumber();

({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
0,
0,
0,
0,
0,
0,
seconds,
milliseconds,
microseconds,
nanoseconds,
increment,
smallestUnit,
roundingMode
));
if (smallestUnit !== 'nanosecond' || increment !== 1) {
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
0,
0,
0,
0,
0,
0,
seconds,
milliseconds,
microseconds,
nanoseconds,
increment,
smallestUnit,
roundingMode
));
}
return BalanceTimeDuration(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit);
}

Expand Down Expand Up @@ -4359,33 +4361,35 @@ export function DifferenceTemporalPlainDateTime(operation, plainDateTime, other,
resolvedOptions
);

const relativeTo = TemporalDateTimeToDate(plainDateTime);
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode,
relativeTo
));
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.largestUnit
));
if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) {
const relativeTo = TemporalDateTimeToDate(plainDateTime);
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode,
relativeTo
));
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.largestUnit
));
}

const Duration = GetIntrinsic('%Temporal.Duration%');
return new Duration(
Expand Down Expand Up @@ -4423,21 +4427,23 @@ export function DifferenceTemporalPlainTime(operation, plainTime, other, options
GetSlot(other, ISO_MICROSECOND),
GetSlot(other, ISO_NANOSECOND)
);
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
0,
0,
0,
0,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode
));
if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) {
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
0,
0,
0,
0,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode
));
}
({ hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
0,
hours,
Expand Down Expand Up @@ -4550,24 +4556,9 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other,
}
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
DifferenceZonedDateTime(ns1, ns2, timeZone, calendar, settings.largestUnit, resolvedOptions));
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode,
zonedDateTime
));
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
AdjustRoundedDurationDays(

if (settings.smallestUnit !== 'nanosecond' || settings.roundingIncrement !== 1) {
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = RoundDuration(
years,
months,
weeks,
Expand All @@ -4583,6 +4574,24 @@ export function DifferenceTemporalZonedDateTime(operation, zonedDateTime, other,
settings.roundingMode,
zonedDateTime
));
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
AdjustRoundedDurationDays(
years,
months,
weeks,
days,
hours,
minutes,
seconds,
milliseconds,
microseconds,
nanoseconds,
settings.roundingIncrement,
settings.smallestUnit,
settings.roundingMode,
zonedDateTime
));
}
}

const Duration = GetIntrinsic('%Temporal.Duration%');
Expand Down
14 changes: 14 additions & 0 deletions polyfill/lib/plaindatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,20 @@ export class PlainDateTime {
let millisecond = GetSlot(this, ISO_MILLISECOND);
let microsecond = GetSlot(this, ISO_MICROSECOND);
let nanosecond = GetSlot(this, ISO_NANOSECOND);
if (roundingIncrement === 1 && smallestUnit === 'nanosecond') {
return ES.CreateTemporalDateTime(
year,
month,
day,
hour,
minute,
second,
millisecond,
microsecond,
nanosecond,
GetSlot(this, CALENDAR)
);
}
({ year, month, day, hour, minute, second, millisecond, microsecond, nanosecond } = ES.RoundISODateTime(
year,
month,
Expand Down
8 changes: 8 additions & 0 deletions polyfill/lib/zoneddatetime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ export class ZonedDateTime {
const inclusive = maximum === 1;
ES.ValidateTemporalRoundingIncrement(roundingIncrement, maximum, inclusive);

if (smallestUnit === 'nanosecond' && roundingIncrement === 1) {
return ES.CreateTemporalZonedDateTime(
GetSlot(this, EPOCHNANOSECONDS),
GetSlot(this, TIME_ZONE),
GetSlot(this, CALENDAR)
);
}

// first, round the underlying DateTime fields
const timeZone = GetSlot(this, TIME_ZONE);
const offsetNs = ES.GetOffsetNanosecondsFor(timeZone, GetSlot(this, INSTANT));
Expand Down
10 changes: 8 additions & 2 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,8 @@ <h1>Temporal.Duration.prototype.round ( _roundTo_ )</h1>
1. If _smallestUnit_ is *undefined*, then
1. Set _smallestUnitPresent_ to *false*.
1. Set _smallestUnit_ to *"nanosecond"*.
1. Let _defaultLargestUnit_ be ! DefaultTemporalLargestUnit(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]]).
1. Set _defaultLargestUnit_ to ! LargerOfTwoTemporalUnits(_defaultLargestUnit_, _smallestUnit_).
1. Let _existingLargestUnit_ be ! DefaultTemporalLargestUnit(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]]).
1. Let _defaultLargestUnit_ be ! LargerOfTwoTemporalUnits(_existingLargestUnit_, _smallestUnit_).
1. If _largestUnit_ is *undefined*, then
1. Set _largestUnitPresent_ to *false*.
1. Set _largestUnit_ to _defaultLargestUnit_.
Expand All @@ -433,6 +433,12 @@ <h1>Temporal.Duration.prototype.round ( _roundTo_ )</h1>
1. If LargerOfTwoTemporalUnits(_largestUnit_, _smallestUnit_) is not _largestUnit_, throw a *RangeError* exception.
1. Let _maximum_ be ! MaximumTemporalDurationRoundingIncrement(_smallestUnit_).
1. If _maximum_ is not *undefined*, perform ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, *false*).
1. Let _hoursToDaysConversionMayOccur_ be *false*.
1. If _duration_.[[Days]] &ne; 0 and _relativeTo_ is an Object with an [[InitializedTemporalZonedDateTime]] internal slot, set _hoursToDaysConversionMayOccur_ to *true*.
1. Else if _duration_.[[Hours]] &ge; 24, set _hoursToDaysConversionMayOccur_ to *true*.
1. If _smallestUnit_ is *"nanosecond"*, and _roundingIncrement_ = 1, and _largestUnit_ is _existingLargestUnit_, and _duration_.[[Years]] = 0, and _duration_.[[Months]] = 0, and _duration_.[[Weeks]] = 0, and _hoursToDaysConversionMayOccur_ is *false*, and _duration_.[[Minutes]] &lt; 60, and _duration_.[[Seconds]] &lt; 60, and _duration_.[[Milliseconds]] &lt; 1000, and _duration_.[[Microseconds]] &lt; 1000, and _duration_.[[Nanoseconds]] &lt; 1000, then
1. NOTE: The above conditions mean that the operation will have no effect: the smallest unit and rounding increment will leave the total duration unchanged, and it can be determined without calling a calendar or time zone method that no balancing will take place.
1. Return ! CreateTemporalDuration(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _duration_.[[Hours]], _duration_.[[Minutes]], _duration_.[[Seconds]], _duration_.[[Milliseconds]], _duration_.[[Microseconds]], _duration_.[[Nanoseconds]]).
1. Let _unbalanceResult_ be ? UnbalanceDateDurationRelative(_duration_.[[Years]], _duration_.[[Months]], _duration_.[[Weeks]], _duration_.[[Days]], _largestUnit_, _relativeTo_).
1. Let _roundRecord_ 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_).
1. Let _roundResult_ be _roundRecord_.[[DurationRecord]].
Expand Down
2 changes: 2 additions & 0 deletions spec/instant.html
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,8 @@ <h1>
1. Let _microseconds_ be remainder(truncate(_difference_ / 1000), 1000).
1. Let _milliseconds_ be remainder(truncate(_difference_ / 10<sup>6</sup>), 1000).
1. Let _seconds_ be truncate(_difference_ / 10<sup>9</sup>).
1. If _smallestUnit_ is *"nanosecond"* and _roundingIncrement_ is 1, then
1. Return ! BalanceTimeDuration(0, 0, 0, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _largestUnit_).
1. Let _roundResult_ be ! RoundDuration(0, 0, 0, 0, 0, 0, _seconds_, _milliseconds_, _microseconds_, _nanoseconds_, _roundingIncrement_, _smallestUnit_, _roundingMode_).
1. Assert: _roundResult_.[[Days]] is 0.
1. Return ! BalanceTimeDuration(0, _roundResult_.[[Hours]], _roundResult_.[[Minutes]], _roundResult_.[[Seconds]], _roundResult_.[[Milliseconds]], _roundResult_.[[Microseconds]], _roundResult_.[[Nanoseconds]], _largestUnit_).
Expand Down
4 changes: 4 additions & 0 deletions spec/plaindatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ <h1>Temporal.PlainDateTime.prototype.round ( _roundTo_ )</h1>
1. Assert: _maximum_ is not *undefined*.
1. Let _inclusive_ be *false*.
1. Perform ? ValidateTemporalRoundingIncrement(_roundingIncrement_, _maximum_, _inclusive_).
1. If _smallestUnit_ is *"nanosecond"* and _roundingIncrement_ is 1, then
1. Return ! CreateTemporalDateTime(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]], _dateTime_.[[Calendar]]).
1. Let _result_ be RoundISODateTime(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]], _roundingIncrement_, _smallestUnit_, _roundingMode_).
1. Return ? CreateTemporalDateTime(_result_.[[Year]], _result_.[[Month]], _result_.[[Day]], _result_.[[Hour]], _result_.[[Minute]], _result_.[[Second]], _result_.[[Millisecond]], _result_.[[Microsecond]], _result_.[[Nanosecond]], _dateTime_.[[Calendar]]).
</emu-alg>
Expand Down Expand Up @@ -1280,6 +1282,8 @@ <h1>
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. Let _settings_ be ? GetDifferenceSettings(_operation_, _resolvedOptions_, ~datetime~, &laquo; &raquo;, *"nanosecond"*, *"day"*).
1. Let _diff_ be ? DifferenceISODateTime(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[ISOHour]], _dateTime_.[[ISOMinute]], _dateTime_.[[ISOSecond]], _dateTime_.[[ISOMillisecond]], _dateTime_.[[ISOMicrosecond]], _dateTime_.[[ISONanosecond]], _other_.[[ISOYear]], _other_.[[ISOMonth]], _other_.[[ISODay]], _other_.[[ISOHour]], _other_.[[ISOMinute]], _other_.[[ISOSecond]], _other_.[[ISOMillisecond]], _other_.[[ISOMicrosecond]], _other_.[[ISONanosecond]], _dateTime_.[[Calendar]], _settings_.[[LargestUnit]], _resolvedOptions_).
1. If _settings_.[[SmallestUnit]] is *"nanosecond"* and _settings_.[[RoundingIncrement]] is 1, then
1. Return ! CreateTemporalDuration(_sign_ &times; _diff_.[[Years]], _sign_ &times; _diff_.[[Months]], _sign_ &times; _diff_.[[Weeks]], _sign_ &times; _diff_.[[Days]], _sign_ &times; _diff_.[[Hours]], _sign_ &times; _diff_.[[Minutes]], _sign_ &times; _diff_.[[Seconds]], _sign_ &times; _diff_.[[Milliseconds]], _sign_ &times; _diff_.[[Microseconds]], _sign_ &times; _diff_.[[Nanoseconds]]).
1. Let _relativeTo_ be ! CreateTemporalDate(_dateTime_.[[ISOYear]], _dateTime_.[[ISOMonth]], _dateTime_.[[ISODay]], _dateTime_.[[Calendar]]).
1. Let _roundRecord_ be ? RoundDuration(_diff_.[[Years]], _diff_.[[Months]], _diff_.[[Weeks]], _diff_.[[Days]], _diff_.[[Hours]], _diff_.[[Minutes]], _diff_.[[Seconds]], _diff_.[[Milliseconds]], _diff_.[[Microseconds]], _diff_.[[Nanoseconds]], _settings_.[[RoundingIncrement]], _settings_.[[SmallestUnit]], _settings_.[[RoundingMode]], _relativeTo_).
1. Let _roundResult_ be _roundRecord_.[[DurationRecord]].
Expand Down
5 changes: 3 additions & 2 deletions spec/plaintime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1018,8 +1018,9 @@ <h1>
1. Let _resolvedOptions_ be ? SnapshotOwnProperties(? GetOptionsObject(_options_), *null*).
1. Let _settings_ be ? GetDifferenceSettings(_operation_, _resolvedOptions_, ~time~, &laquo; &raquo;, *"nanosecond"*, *"hour"*).
1. Let _result_ be ! DifferenceTime(_temporalTime_.[[ISOHour]], _temporalTime_.[[ISOMinute]], _temporalTime_.[[ISOSecond]], _temporalTime_.[[ISOMillisecond]], _temporalTime_.[[ISOMicrosecond]], _temporalTime_.[[ISONanosecond]], _other_.[[ISOHour]], _other_.[[ISOMinute]], _other_.[[ISOSecond]], _other_.[[ISOMillisecond]], _other_.[[ISOMicrosecond]], _other_.[[ISONanosecond]]).
1. Let _roundRecord_ be ! RoundDuration(0, 0, 0, 0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]], _settings_.[[RoundingIncrement]], _settings_.[[SmallestUnit]], _settings_.[[RoundingMode]]).
1. Set _result_ to _roundRecord_.[[DurationRecord]].
1. If _settings_.[[SmallestUnit]] is not *"nanosecond"* or _settings_.[[RoundingIncrement]] &ne; 1, then
1. Let _roundRecord_ be ! RoundDuration(0, 0, 0, 0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]], _settings_.[[RoundingIncrement]], _settings_.[[SmallestUnit]], _settings_.[[RoundingMode]]).
1. Set _result_ to _roundRecord_.[[DurationRecord]].
1. Set _result_ to ! BalanceTimeDuration(0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]], _settings_.[[LargestUnit]]).
1. Return ! CreateTemporalDuration(0, 0, 0, 0, _sign_ &times; _result_.[[Hours]], _sign_ &times; _result_.[[Minutes]], _sign_ &times; _result_.[[Seconds]], _sign_ &times; _result_.[[Milliseconds]], _sign_ &times; _result_.[[Microseconds]], _sign_ &times; _result_.[[Nanoseconds]]).
</emu-alg>
Expand Down
Loading

0 comments on commit 51ea969

Please sign in to comment.