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 15, 2024
1 parent a113bda commit cd2d12b
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 176 deletions.
65 changes: 31 additions & 34 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.
- `options` (optional object): An object with properties representing options for the addition.
The following option is recognized:
- `relativeTo` (`Temporal.PlainDate`, `Temporal.ZonedDateTime`, or value convertible to one of those): The starting point to use when adding years, months, weeks, and days.

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

Expand All @@ -275,13 +272,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.

Adding a negative duration is equivalent to subtracting the absolute value of that duration.

Expand All @@ -298,27 +291,27 @@ 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

// 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
// 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

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

<!-- prettier-ignore-start -->

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

**Parameters:**

- `other` (`Temporal.Duration` or value convertible to one): The duration to subtract.
- `options` (optional object): An object with properties representing options for the 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 years, months, weeks, and days.

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

Expand All @@ -330,13 +323,9 @@ If `duration` is not a `Temporal.Duration` object, then it will be converted to
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#duration-arithmetic).

By default, you cannot subtract 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.

Subtracting a negative duration is equivalent to adding the absolute value of that duration.

Expand All @@ -351,12 +340,20 @@ two = Temporal.Duration.from({ seconds: 30 });
one.subtract(two); // => PT179M30S
one.subtract(two).round({ largestUnit: 'hour' }); // => PT2H59M30S

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

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

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

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

/**
* Options to control behavior of `Duration.compare()`, `Duration.add()`, and
* `Duration.subtract()`
* Options to control behavior of `Duration.compare()`
*/
export interface DurationArithmeticOptions {
/**
Expand Down Expand Up @@ -544,8 +543,8 @@ export namespace Temporal {
negated(): Temporal.Duration;
abs(): Temporal.Duration;
with(durationLike: DurationLike): Temporal.Duration;
add(other: Temporal.Duration | DurationLike | string, options?: DurationArithmeticOptions): Temporal.Duration;
subtract(other: Temporal.Duration | DurationLike | string, options?: DurationArithmeticOptions): Temporal.Duration;
add(other: Temporal.Duration | DurationLike | string): Temporal.Duration;
subtract(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
8 changes: 4 additions & 4 deletions polyfill/lib/duration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,13 @@ 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('add', this, other, options);
return ES.AddDurations('add', this, other);
}
subtract(other, options = undefined) {
subtract(other) {
if (!ES.IsTemporalDuration(this)) throw new TypeError('invalid receiver');
return ES.AddDurations('subtract', this, other, options);
return ES.AddDurations('subtract', 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 @@ -5091,16 +5091,9 @@ export function AddDaysToZonedDateTime(instant, dateTime, timeZoneRec, calendar,
};
}

export function AddDurations(operation, duration, other, options) {
export function AddDurations(operation, duration, other) {
const sign = operation === 'subtract' ? -1 : 1;
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 @@ -5131,87 +5124,14 @@ export function AddDurations(operation, 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);
if (IsCalendarUnit(largestUnit)) {
throw new RangeError('For years, months, or weeks arithmetic, use date arithmetic relative to a starting point');
}
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);
}

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(operation, instant, durationLike) {
Expand Down
Loading

0 comments on commit cd2d12b

Please sign in to comment.