Skip to content

Commit

Permalink
Normative: Combine code paths for duration rounding and difference
Browse files Browse the repository at this point in the history
In order to prevent bugs due to discrepancies between two ways of
calculating the same thing such as in #2742, refactor duration rounding
with relativeTo so that

    duration.round({ smallestUnit, largestUnit, relativeTo, ...options })

goes through the same code path and gives the same result as

    const target = relativeTo.add(duration);
    relativeTo.until(target, { smallestUnit, largestUnit, ...options })

but taking into account that the until() methods have a different default
roundingMode than Duration.prototype.round(), and optimizing away as many
user-observable calls as possible.

Similarly,

    duration.total({ unit, relativeTo, ...options })

goes through the same code path, which also returns the total as a
mathematical value if needed.
  • Loading branch information
ptomato committed Feb 1, 2024
1 parent f43ead4 commit cf03c63
Show file tree
Hide file tree
Showing 6 changed files with 574 additions and 301 deletions.
293 changes: 177 additions & 116 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* global __debug__ */

import * as ES from './ecmascript.mjs';
import { MakeIntrinsicClass } from './intrinsicclass.mjs';
import { GetIntrinsic, MakeIntrinsicClass } from './intrinsicclass.mjs';
import { CalendarMethodRecord } from './methodrecord.mjs';
import {
YEARS,
Expand All @@ -17,6 +17,9 @@ import {
CALENDAR,
INSTANT,
EPOCHNANOSECONDS,
ISO_YEAR,
ISO_MONTH,
ISO_DAY,
CreateSlots,
GetSlot,
SetSlot
Expand Down Expand Up @@ -327,79 +330,106 @@ export class Duration {
'dateUntil'
]);

({ years, months, weeks, days } = ES.UnbalanceDateDurationRelative(
years,
months,
weeks,
days,
largestUnit,
plainRelativeTo,
calendarRec
));
let norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
({ years, months, weeks, days, norm } = ES.RoundDuration(
years,
months,
weeks,
days,
norm,
roundingIncrement,
smallestUnit,
roundingMode,
plainRelativeTo,
calendarRec,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime
));

if (zonedRelativeTo) {
({ years, months, weeks, days, norm } = ES.AdjustRoundedDurationDays(
years,
months,
weeks,
days,
norm,
roundingIncrement,
smallestUnit,
roundingMode,
zonedRelativeTo,
calendarRec,
const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS);
const targetEpochNs = ES.AddZonedDateTime(
GetSlot(zonedRelativeTo, INSTANT),
timeZoneRec,
precalculatedPlainDateTime
));
const intermediate = ES.MoveRelativeZonedDateTime(
zonedRelativeTo,
calendarRec,
timeZoneRec,
years,
months,
weeks,
0,
precalculatedPlainDateTime
);
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDurationRelative(
days,
norm,
largestUnit,
intermediate,
timeZoneRec
));
precalculatedPlainDateTime
);
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
ES.DifferenceZonedDateTimeWithRounding(
relativeEpochNs,
targetEpochNs,
plainRelativeTo,
calendarRec,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime,
ObjectCreate(null),
largestUnit,
roundingIncrement,
smallestUnit,
roundingMode
));
} else if (plainRelativeTo) {
let targetTime = ES.AddTime(0, 0, 0, 0, 0, 0, norm);

// Delegate the date part addition to the calendar
const TemporalDuration = GetIntrinsic('%Temporal.Duration%');
const dateDuration = new TemporalDuration(years, months, weeks, days + targetTime.deltaDays, 0, 0, 0, 0, 0, 0);
const targetDate = ES.AddDate(calendarRec, plainRelativeTo, dateDuration);

({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
ES.DifferencePlainDateTimeWithRounding(
plainRelativeTo,
0,
0,
0,
0,
0,
0,
GetSlot(targetDate, ISO_YEAR),
GetSlot(targetDate, ISO_MONTH),
GetSlot(targetDate, ISO_DAY),
targetTime.hour,
targetTime.minute,
targetTime.second,
targetTime.millisecond,
targetTime.microsecond,
targetTime.nanosecond,
calendarRec,
largestUnit,
roundingIncrement,
smallestUnit,
roundingMode
));
} else {
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = ES.BalanceTimeDuration(
norm.add24HourDays(days),
largestUnit
));
if (calendarUnitsPresent) {
throw new RangeError('a starting point is required for years, months, or weeks balancing');
}
if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week') {
throw new RangeError(`a starting point is required for ${largestUnit}s balancing`);
}
if (smallestUnit === 'year' || smallestUnit === 'month' || smallestUnit === 'week') {
throw new RangeError(`a starting point is required for ${smallestUnit}s rounding`);
}

const isoCalendarRec = new CalendarMethodRecord('iso8601', ['dateAdd', 'dateUntil']);
const target = ES.AddDateTime(1970, 1, 1, 0, 0, 0, 0, 0, 0, isoCalendarRec, years, months, weeks, days, norm);
({ years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } =
ES.DifferencePlainDateTimeWithRounding(
undefined,
0,
0,
0,
0,
0,
0,
target.year,
target.month,
target.day,
target.hour,
target.minute,
target.second,
target.millisecond,
target.microsecond,
target.nanosecond,
isoCalendarRec,
largestUnit,
roundingIncrement,
smallestUnit,
roundingMode
));
}
({ years, months, weeks, days } = ES.BalanceDateDurationRelative(
years,
months,
weeks,
days,
largestUnit,
smallestUnit,
plainRelativeTo,
calendarRec
));

return new Duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}
Expand Down Expand Up @@ -446,69 +476,100 @@ export class Duration {
'dateUntil'
]);

// Convert larger units down to days
({ years, months, weeks, days } = ES.UnbalanceDateDurationRelative(
years,
months,
weeks,
days,
unit,
plainRelativeTo,
calendarRec
));
let norm;
// If the unit we're totalling is smaller than `days`, convert days down to that unit.
let norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
let total;
if (zonedRelativeTo) {
const intermediate = ES.MoveRelativeZonedDateTime(
zonedRelativeTo,
calendarRec,
const relativeEpochNs = GetSlot(zonedRelativeTo, EPOCHNANOSECONDS);
const targetEpochNs = ES.AddZonedDateTime(
GetSlot(zonedRelativeTo, INSTANT),
timeZoneRec,
calendarRec,
years,
months,
weeks,
0,
days,
norm,
precalculatedPlainDateTime
);
norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);

// Inline BalanceTimeDurationRelative, without the final balance step
const start = GetSlot(intermediate, INSTANT);
const startNs = GetSlot(intermediate, EPOCHNANOSECONDS);
let intermediateNs = startNs;
let startDt;
if (days !== 0) {
startDt = ES.GetPlainDateTimeFor(timeZoneRec, start, 'iso8601');
intermediateNs = ES.AddDaysToZonedDateTime(start, startDt, timeZoneRec, 'iso8601', days).epochNs;
({ total } = ES.DifferenceZonedDateTimeWithRounding(
relativeEpochNs,
targetEpochNs,
plainRelativeTo,
calendarRec,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime,
ObjectCreate(null),
unit,
1,
unit,
'trunc'
));
} else if (plainRelativeTo) {
let targetTime = ES.AddTime(0, 0, 0, 0, 0, 0, norm);

// Delegate the date part addition to the calendar
const TemporalDuration = GetIntrinsic('%Temporal.Duration%');
const dateDuration = new TemporalDuration(years, months, weeks, days + targetTime.deltaDays, 0, 0, 0, 0, 0, 0);
const targetDate = ES.AddDate(calendarRec, plainRelativeTo, dateDuration);

({ total } = ES.DifferencePlainDateTimeWithRounding(
plainRelativeTo,
0,
0,
0,
0,
0,
0,
GetSlot(targetDate, ISO_YEAR),
GetSlot(targetDate, ISO_MONTH),
GetSlot(targetDate, ISO_DAY),
targetTime.hour,
targetTime.minute,
targetTime.second,
targetTime.millisecond,
targetTime.microsecond,
targetTime.nanosecond,
calendarRec,
unit,
1,
unit,
'trunc'
));
} else {
if (years !== 0 || months !== 0 || weeks !== 0) {
throw new RangeError('a starting point is required for years, months, or weeks total');
}
const endNs = ES.AddInstant(intermediateNs, norm);
norm = TimeDuration.fromEpochNsDiff(endNs, startNs);
if (ES.IsCalendarUnit(unit) || unit === 'day') {
if (!norm.isZero()) startDt ??= ES.GetPlainDateTimeFor(timeZoneRec, start, 'iso8601');
({ days, norm } = ES.NormalizedTimeDurationToDays(norm, intermediate, timeZoneRec, startDt));
} else {
days = 0;
if (unit === 'year' || unit === 'month' || unit === 'week') {
throw new RangeError(`a starting point is required for ${unit}s total`);
}
} else {
norm = TimeDuration.normalize(hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
norm = norm.add24HourDays(days);
days = 0;

const isoCalendarRec = new CalendarMethodRecord('iso8601', ['dateAdd', 'dateUntil']);
const target = ES.AddDateTime(1970, 1, 1, 0, 0, 0, 0, 0, 0, isoCalendarRec, years, months, weeks, days, norm);
({ total } = ES.DifferencePlainDateTimeWithRounding(
undefined,
0,
0,
0,
0,
0,
0,
target.year,
target.month,
target.day,
target.hour,
target.minute,
target.second,
target.millisecond,
target.microsecond,
target.nanosecond,
isoCalendarRec,
unit,
1,
unit,
'trunc'
));
}
// Finally, truncate to the correct unit and calculate remainder
const { total } = ES.RoundDuration(
years,
months,
weeks,
days,
norm,
1,
unit,
'trunc',
plainRelativeTo,
calendarRec,
zonedRelativeTo,
timeZoneRec,
precalculatedPlainDateTime
);
return total;
}
toString(options = undefined) {
Expand Down
Loading

0 comments on commit cf03c63

Please sign in to comment.