Skip to content

Commit

Permalink
Normative: Remove relativeTo option from Duration.p.add/subtract
Browse files Browse the repository at this point in the history
Removes the options parameter from Temporal.Duration.prototype.add and
Temporal.Duration.prototype.subtract. Everything else remains the same:
Additions and subtractions that previously succeeded without relativeTo,
still succeed, with the same results. Additions and subtractions that
previously threw if there was no relativeTo, now just throw
unconditionally.

Closes: #2825
  • Loading branch information
ptomato committed May 27, 2024
1 parent 528b7a0 commit 245c221
Show file tree
Hide file tree
Showing 5 changed files with 47 additions and 159 deletions.
50 changes: 27 additions & 23 deletions docs/duration.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,14 +256,11 @@ duration = duration.with({ years, months });
```
<!-- prettier-ignore-end -->

### duration.**add**(_other_: Temporal.Duration | object | string, _options_?: object) : Temporal.Duration
### duration.**add**(_other_: Temporal.Duration | object | string) : Temporal.Duration

**Parameters:**

- `other` (`Temporal.Duration` or value convertible to one): The duration to add, or subtract if negative.
- `options` (optional object): An object with properties representing options for the addition or subtraction.
The following option is recognized:
- `relativeTo` (`Temporal.PlainDate`, `Temporal.ZonedDateTime`, or value convertible to one of those): The starting point to use when adding or subtracting years, months, weeks, and days.

**Returns:** a new `Temporal.Duration` object which represents the sum of the durations of `duration` and `other`.

Expand All @@ -279,13 +276,9 @@ If `other` is not a `Temporal.Duration` object, then it will be converted to one
In order to be valid, the resulting duration must not have fields with mixed signs, and so the result is balanced.
For usage examples and a more complete explanation of how balancing works and why it is necessary, see [Duration balancing](./balancing.md).

By default, you cannot add durations with years, months, or weeks, as that could be ambiguous depending on the start date.
To do this, you must provide a start date using the `relativeTo` option.

The `relativeTo` option may be a `Temporal.ZonedDateTime` in which case time zone offset changes will be taken into account when converting between days and hours. If `relativeTo` is omitted or is a `Temporal.PlainDate`, then days are always considered equal to 24 hours.

If `relativeTo` is neither a `Temporal.PlainDate` nor a `Temporal.ZonedDateTime`, then it will be converted to one of the two, as if it were first attempted with `Temporal.ZonedDateTime.from()` and then with `Temporal.PlainDate.from()`.
This means that an ISO 8601 string with a time zone name annotation in it, or a property bag with a `timeZone` property, will be converted to a `Temporal.ZonedDateTime`, and an ISO 8601 string without a time zone name or a property bag without a `timeZone` property will be converted to a `Temporal.PlainDate`.
You cannot convert between years, months, or weeks when adding durations, as that could be ambiguous depending on the start date.
If `duration` or `other` have nonzero years, months, or weeks, this function will throw an exception.
If you need to add durations with years, months, or weeks, add the two durations to a start date, and then figure the difference between the resulting date and the start date.

Usage example:

Expand All @@ -300,15 +293,18 @@ one = Temporal.Duration.from({ hours: 1, minutes: 30 });
two = Temporal.Duration.from({ hours: 2, minutes: 45 });
result = one.add(two); // => PT4H15M

fifty = Temporal.Duration.from('P50Y50M50DT50H50M50.500500500S');
/* WRONG */ result = fifty.add(fifty); // => throws, need relativeTo
result = fifty.add(fifty, { relativeTo: '1900-01-01' }); // => P108Y7M12DT5H41M41.001001S
// Example of adding calendar units
oneAndAHalfMonth = Temporal.Duration.from({ months: 1, days: 16 });
/* WRONG */ oneAndAHalfMonth.add(oneAndAHalfMonth); // => not allowed, throws

// To convert units, use arithmetic relative to a start date:
startDate1 = Temporal.PlainDate.from('2000-12-01');
startDate1.add(oneAndAHalfMonth).add(oneAndAHalfMonth)
.since(startDate1, { largestUnit: 'months' }); // => P3M4D

// Example of converting ambiguous units relative to a start date
oneAndAHalfMonth = Temporal.Duration.from({ months: 1, days: 15 });
/* WRONG */ oneAndAHalfMonth.add(oneAndAHalfMonth); // => throws
oneAndAHalfMonth.add(oneAndAHalfMonth, { relativeTo: '2000-02-01' }); // => P3M
oneAndAHalfMonth.add(oneAndAHalfMonth, { relativeTo: '2000-03-01' }); // => P2M30D
startDate2 = Temporal.PlainDate.from('2001-01-01');
startDate2.add(oneAndAHalfMonth).add(oneAndAHalfMonth)
.since(startDate2, { largestUnit: 'months' }); // => P3M1D

// Example of subtraction:
hourAndAHalf = Temporal.Duration.from('PT1H30M');
Expand All @@ -319,12 +315,20 @@ two = Temporal.Duration.from({ seconds: 30 });
one.add(two.negated()); // => PT179M30S
one.add(two.negated()).round({ largestUnit: 'hour' }); // => PT2H59M30S

// Example of converting ambiguous units relative to a start date
// Example of subtracting calendar units; cannot be subtracted using
// add() because units need to be converted
threeMonths = Temporal.Duration.from({ months: 3 });
oneAndAHalfMonthNegated = Temporal.Duration.from({ months: -1, days: -15 });
/* WRONG */ threeMonths.add(oneAndAHalfMonthNegated); // => throws
threeMonths.add(oneAndAHalfMonthNegated, { relativeTo: '2000-02-01' }); // => P1M16D
threeMonths.add(oneAndAHalfMonthNegated, { relativeTo: '2000-03-01' }); // => P1M15D
/* WRONG */ threeMonths.add(oneAndAHalfMonthNegated); // => not allowed, throws

// To convert units, use arithmetic relative to a start date:
startDate1 = Temporal.PlainDate.from('2001-01-01');
startDate1.add(threeMonths).add(oneAndAHalfMonthNegated)
.since(startDate1, { largestUnit: 'months' }); // => P1M13D

startDate2 = Temporal.PlainDate.from('2001-02-01');
startDate2.add(threeMonths).add(oneAndAHalfMonthNegated)
.since(startDate2, { largestUnit: 'months' }); // => P1M16D
```

### duration.**negated**() : Temporal.Duration
Expand Down
4 changes: 2 additions & 2 deletions polyfill/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ export namespace Temporal {
};

/**
* Options to control behavior of `Duration.compare()`, `Duration.add()`
* Options to control behavior of `Duration.compare()`
*/
export interface DurationArithmeticOptions {
/**
Expand Down Expand Up @@ -542,7 +542,7 @@ export namespace Temporal {
negated(): Temporal.Duration;
abs(): Temporal.Duration;
with(durationLike: DurationLike): Temporal.Duration;
add(other: Temporal.Duration | DurationLike | string, options?: DurationArithmeticOptions): Temporal.Duration;
add(other: Temporal.Duration | DurationLike | string): Temporal.Duration;
round(roundTo: DurationRoundTo): Temporal.Duration;
total(totalOf: DurationTotalOf): number;
toLocaleString(locales?: string | string[], options?: Intl.DateTimeFormatOptions): string;
Expand Down
4 changes: 2 additions & 2 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,9 @@ export class Duration {
Math.abs(GetSlot(this, NANOSECONDS))
);
}
add(other, options = undefined) {
add(other) {
if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver');
return ES.AddDurations(this, other, options);
return ES.AddDurations(this, other);
}
round(roundTo) {
if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver');
Expand Down
94 changes: 7 additions & 87 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5012,15 +5012,8 @@ export function AddDaysToZonedDateTime(instant, dateTime, timeZoneRec, calendar,
};
}

export function AddDurations(duration, other, options) {
export function AddDurations(duration, other) {
other = ToTemporalDurationRecord(other);
options = GetOptionsObject(options);
const { plainRelativeTo, zonedRelativeTo, timeZoneRec } = GetTemporalRelativeToOption(options);

const calendarRec = CalendarMethodRecord.CreateFromRelativeTo(plainRelativeTo, zonedRelativeTo, [
'dateAdd',
'dateUntil'
]);

const y1 = GetSlot(duration, YEARS);
const mon1 = GetSlot(duration, MONTHS);
Expand Down Expand Up @@ -5051,87 +5044,14 @@ export function AddDurations(duration, other, options) {
const norm2 = TimeDuration.normalize(h2, min2, s2, ms2, µs2, ns2);
const Duration = GetIntrinsic('%Temporal.Duration%');

if (!zonedRelativeTo && !plainRelativeTo) {
if (IsCalendarUnit(largestUnit)) {
throw new RangeError('relativeTo is required for years, months, or weeks arithmetic');
}
const { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
norm1.add(norm2).add24HourDays(d1 + d2),
largestUnit
);
return new Duration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}

if (plainRelativeTo) {
const dateDuration1 = new Duration(y1, mon1, w1, d1, 0, 0, 0, 0, 0, 0);
const dateDuration2 = new Duration(y2, mon2, w2, d2, 0, 0, 0, 0, 0, 0);
const intermediate = AddDate(calendarRec, plainRelativeTo, dateDuration1);
const end = AddDate(calendarRec, intermediate, dateDuration2);

const dateLargestUnit = LargerOfTwoTemporalUnits('day', largestUnit);
const differenceOptions = ObjectCreate(null);
differenceOptions.largestUnit = dateLargestUnit;
const untilResult = DifferenceDate(calendarRec, plainRelativeTo, end, differenceOptions);
const years = GetSlot(untilResult, YEARS);
const months = GetSlot(untilResult, MONTHS);
const weeks = GetSlot(untilResult, WEEKS);
let days = GetSlot(untilResult, DAYS);
// Signs of date part and time part may not agree; balance them together
let hours, minutes, seconds, milliseconds, microseconds, nanoseconds;
({ days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
norm1.add(norm2).add24HourDays(days),
largestUnit
));
return new Duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}

// zonedRelativeTo is defined
const TemporalInstant = GetIntrinsic('%Temporal.Instant%');
const calendar = GetSlot(zonedRelativeTo, CALENDAR);
const startInstant = GetSlot(zonedRelativeTo, INSTANT);
let startDateTime;
if (IsCalendarUnit(largestUnit) || largestUnit === 'day') {
startDateTime = GetPlainDateTimeFor(timeZoneRec, startInstant, calendar);
}
const intermediateNs = AddZonedDateTime(
startInstant,
timeZoneRec,
calendarRec,
y1,
mon1,
w1,
d1,
norm1,
startDateTime
);
const endNs = AddZonedDateTime(
new TemporalInstant(intermediateNs),
timeZoneRec,
calendarRec,
y2,
mon2,
w2,
d2,
norm2
);
if (largestUnit !== 'year' && largestUnit !== 'month' && largestUnit !== 'week' && largestUnit !== 'day') {
// The user is only asking for a time difference, so return difference of instants.
const norm = TimeDuration.fromEpochNsDiff(endNs, GetSlot(zonedRelativeTo, EPOCHNANOSECONDS));
const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, largestUnit);
return new Duration(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
if (IsCalendarUnit(largestUnit)) {
throw new RangeError('For years, months, or weeks arithmetic, use date arithmetic relative to a starting point');
}

const { years, months, weeks, days, norm } = DifferenceZonedDateTime(
GetSlot(zonedRelativeTo, EPOCHNANOSECONDS),
endNs,
timeZoneRec,
calendarRec,
largestUnit,
ObjectCreate(null),
startDateTime
const { days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(
norm1.add(norm2).add24HourDays(d1 + d2),
largestUnit
);
const { hours, minutes, seconds, milliseconds, microseconds, nanoseconds } = BalanceTimeDuration(norm, 'hour');
return new Duration(years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
return new Duration(0, 0, 0, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds);
}

export function AddDurationToOrSubtractDurationFromInstant(instant, durationLike) {
Expand Down
54 changes: 9 additions & 45 deletions spec/duration.html
Original file line number Diff line number Diff line change
Expand Up @@ -386,14 +386,14 @@ <h1>Temporal.Duration.prototype.abs ( )</h1>
</emu-clause>

<emu-clause id="sec-temporal.duration.prototype.add">
<h1>Temporal.Duration.prototype.add ( _other_ [ , _options_ ] )</h1>
<h1>Temporal.Duration.prototype.add ( _other_ )</h1>
<p>
The `Temporal.Duration.prototype.add` method performs the following steps when called:
</p>
<emu-alg>
1. Let _duration_ be the *this* value.
1. Perform ? RequireInternalSlot(_duration_, [[InitializedTemporalDuration]]).
1. Return ? AddDurations(_duration_, _other_, _options_).
1. Return ? AddDurations(_duration_, _other_).
</emu-alg>
</emu-clause>

Expand Down Expand Up @@ -2119,24 +2119,17 @@ <h1>
AddDurations (
_duration_: a Temporal.Duration,
_other_: an ECMAScript language value,
_options_: an ECMAScript language value,
): either a normal completion containing a Temporal.Duration or a throw completion
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It adds or subtracts the components of a second duration _other_ to or from those of a first duration _duration_, resulting in a longer or shorter duration.
It balances the result relative to the given `relativeTo` option, to ensure that no mixed signs remain in the result.
It adds or subtracts the components of a second duration _other_ to or from those of a first duration _duration_, resulting in a longer or shorter duration, unless calendar calculations would be required, in which case it throws an exception.
It balances the result, ensuring that no mixed signs remain.
</dd>
</dl>
<emu-alg>
1. Set _other_ to ? ToTemporalDurationRecord(_other_).
1. Set _options_ to ? GetOptionsObject(_options_).
1. Let _relativeToRecord_ be ? GetTemporalRelativeToOption(_options_).
1. Let _plainRelativeTo_ be _relativeToRecord_.[[PlainRelativeTo]].
1. Let _zonedRelativeTo_ be _relativeToRecord_.[[ZonedRelativeTo]].
1. Let _timeZoneRec_ be _relativeToRecord_.[[TimeZoneRec]].
1. Let _calendarRec_ be ? CreateCalendarMethodsRecordFromRelativeTo(_plainRelativeTo_, _zonedRelativeTo_, « ~date-add~, ~date-until~ »).
1. Let _y1_ be _duration_.[[Years]].
1. Let _mon1_ be _duration_.[[Months]].
1. Let _w1_ be _duration_.[[Weeks]].
Expand All @@ -2162,40 +2155,11 @@ <h1>
1. Let _largestUnit_ be LargerOfTwoTemporalUnits(_largestUnit1_, _largestUnit2_).
1. Let _norm1_ be NormalizeTimeDuration(_h1_, _min1_, _s1_, _ms1_, _mus1_, _ns1_).
1. Let _norm2_ be NormalizeTimeDuration(_h2_, _min2_, _s2_, _ms2_, _mus2_, _ns2_).
1. If _zonedRelativeTo_ is *undefined* and _plainRelativeTo_ is *undefined*, then
1. If IsCalendarUnit(_largestUnit_), throw a *RangeError* exception.
1. Let _normResult_ be ? AddNormalizedTimeDuration(_norm1_, _norm2_).
1. Set _normResult_ to ? Add24HourDaysToNormalizedTimeDuration(_normResult_, _d1_ + _d2_).
1. Let _result_ be ? BalanceTimeDuration(_normResult_, _largestUnit_).
1. Return ! CreateTemporalDuration(0, 0, 0, _result_.[[Days]], _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
1. If _plainRelativeTo_ is not *undefined*, then
1. Let _dateDuration1_ be ! CreateTemporalDuration(_y1_, _mon1_, _w1_, _d1_, 0, 0, 0, 0, 0, 0).
1. Let _dateDuration2_ be ! CreateTemporalDuration(_y2_, _mon2_, _w2_, _d2_, 0, 0, 0, 0, 0, 0).
1. Let _intermediate_ be ? AddDate(_calendarRec_, _plainRelativeTo_, _dateDuration1_).
1. Let _end_ be ? AddDate(_calendarRec_, _intermediate_, _dateDuration2_).
1. Let _dateLargestUnit_ be LargerOfTwoTemporalUnits(*"day"*, _largestUnit_).
1. Let _differenceOptions_ be OrdinaryObjectCreate(*null*).
1. Perform ! CreateDataPropertyOrThrow(_differenceOptions_, *"largestUnit"*, _dateLargestUnit_).
1. Let _dateDifference_ be ? DifferenceDate(_calendarRec_, _plainRelativeTo_, _end_, _differenceOptions_).
1. Let _norm1WithDays_ be ? Add24HourDaysToNormalizedTimeDuration(_norm1_, _dateDifference_.[[Days]]).
1. Let _normResult_ be ? AddNormalizedTimeDuration(_norm1WithDays_, _norm2_).
1. Let _result_ be ? BalanceTimeDuration(_normResult_, _largestUnit_).
1. Return ! CreateTemporalDuration(_dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _result_.[[Days]], _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
1. Assert: _zonedRelativeTo_ is not *undefined*.
1. Let _largestUnitCategory_ be the value in the "Category" column of the row of <emu-xref href="#table-temporal-units"></emu-xref> whose "Singular" column contains _largestUnit_.
1. If _largestUnitCategory_ is ~date~, then
1. Let _startDateTime_ be ? GetPlainDateTimeFor(_timeZoneRec_, _zonedRelativeTo_.[[Nanoseconds]], _calendarRec_.[[Receiver]]).
1. Else,
1. Let _startDateTime_ be *undefined*.
1. Let _intermediateNs_ be ? AddZonedDateTime(_zonedRelativeTo_.[[Nanoseconds]], _timeZoneRec_, _calendarRec_, _y1_, _mon1_, _w1_, _d1_, _norm1_, _startDateTime_).
1. Let _endNs_ be ? AddZonedDateTime(_intermediateNs_, _timeZoneRec_, _calendarRec_, _y2_, _mon2_, _w2_, _d2_, _norm2_).
1. If _largestUnitCategory_ is ~time~, then
1. Let _norm_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_endNs_, _zonedRelativeTo_.[[Nanoseconds]]).
1. Let _result_ be ? BalanceTimeDuration(_norm_, _largestUnit_).
1. Return ! CreateTemporalDuration(0, 0, 0, 0, _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
1. Let _diffResult_ be ? DifferenceZonedDateTime(_zonedRelativeTo_.[[Nanoseconds]], _endNs_, _timeZoneRec_, _calendarRec_, _largestUnit_, OrdinaryObjectCreate(*null*), _startDateTime_).
1. Let _timeResult_ be ! BalanceTimeDuration(_diffResult_.[[NormalizedTime]], *"hour"*).
1. Return ! CreateTemporalDuration(_diffResult_.[[Years]], _diffResult_.[[Months]], _diffResult_.[[Weeks]], _diffResult_.[[Days]], _timeResult_.[[Hours]], _timeResult_.[[Minutes]], _timeResult_.[[Seconds]], _timeResult_.[[Milliseconds]], _timeResult_.[[Microseconds]], _timeResult_.[[Nanoseconds]]).
1. If IsCalendarUnit(_largestUnit_), throw a *RangeError* exception.
1. Let _normResult_ be ? AddNormalizedTimeDuration(_norm1_, _norm2_).
1. Set _normResult_ to ? Add24HourDaysToNormalizedTimeDuration(_normResult_, _d1_ + _d2_).
1. Let _result_ be ? BalanceTimeDuration(_normResult_, _largestUnit_).
1. Return ! CreateTemporalDuration(0, 0, 0, _result_.[[Days]], _result_.[[Hours]], _result_.[[Minutes]], _result_.[[Seconds]], _result_.[[Milliseconds]], _result_.[[Microseconds]], _result_.[[Nanoseconds]]).
</emu-alg>
</emu-clause>
</emu-clause>
Expand Down

0 comments on commit 245c221

Please sign in to comment.