-
Notifications
You must be signed in to change notification settings - Fork 172
Description
Today we reached tentative consensus in favor of offering negative durations in Temporal. There was a long discussion in #558 about whether we should do this, but now that we're moving forward I wanted to open a separate issue for how exactly negative durations should work. That's this issue.
1. A Duration can be negative.
2. All Duration units must share the same sign; intra-duration sign variation is not supported
- 2.1 Intra-duration sign variation (e.g. "2 days and negative 12 hours") seems to have only one major use case: the ability to combine a math operation with construction of a
Duration. We explicitly stopped doing this in Remove{disambiguation: 'balance'}inwith()andfrom()of non-Durationtypes #642, so I don't see any need to support it now. The (easy) workaround is to construct a single-sign duration, and then apply the math operation.
3. The string persistence format for Duration will be extended with an optional leading sign
- 3.1 The string format can optionally include a leading minus or (no-op) plus sign, e.g.
-P2Dmeans a negative 2-day duration, whileP2Dand+P2Dboth mean a positive 2-day duration. - 3.2 The leading plus/minus format is used by RFC 5545 and many other libraries and platforms. AFAIK there is no other alternative format used in mainstream platforms.
- 3.3
Duration.prototype.toStringwill emit the leading negative sign for negative durations, but will NOT emit a leading plus for positive durations, so that users who are using ISO8601-compliant positive duration will get an ISO8601-compliant string persistence format. - 3.4
Duration.fromwill accept a leading minus, a leading plus, or no leading sign. Intra-duration (non-leading) plus or minus characters are not supported and must throw when parsed.
4. If a Duration is negative, its nonzero fields will all be negative too.
- 4.1 Methods that accept or return
Durationfields (property getters,getFields,from, andwith) will emit and accept only negative integer values for every nonzero unit of a negative duration.- 4.1.1 The alternative would be for all fields to be positive and rely on a
signfield. The main problem with this approach is that it makeswithseem ambiguous. If you have a duration-P2Dand you say.with({days: 1})do you mean that the resulting duration should be positive or negative? We'd define it to mean the latter, but this seems like it'd be a source of confusion. If the sign is represented in every unit, then there's no ambiguity about the meaning of a negative or positive field value. - 4.1.2 Another disadvantage of all-positive units: it's easy to accidentally "lose" the sign info. For example, the code below would break for negative durations:
- 4.1.1 The alternative would be for all fields to be positive and rely on a
const getDateDuration = d => { years: d.years, months: d.months, weeks: d.weeks, days: d.days };- 4.1.3 Another disadvantage of having all-positive fields is ergonomics. If all fields were positive, then a very large % of calls to property getters would also need to fetch the sign. Example:
const totalDays = dur1.days + dur2.days;
const harderTotalDays = dur1.days*dur1.sign + dur2.days*dur2.sign;- 4.2 Any method that accepts a "Duration-like" property bag (
Duration.from,Duration.prototype.with, or any type'splusorminusmethod) should throw if any of the non-zero input units have different signs. - 4.3 The
Durationconstructor must throw if it's passed non-zero units with different signs. - 4.4
Duration.withcan reverse the sign of a duration, but only if all of the existing duration's non-zero units are replaced.
Duration.from('-P2DT12H').with({weeks: 3, days: 0, hours: 12}); // OK
Duration.from('-P2DT12H').with({weeks: 3, days: 0})`; // throws5. Duration should gain a few convenience properties/methods
- 5.1
Duration.prototype.negated()- reverse the sign - 5.2
Duration.prototype.sign- 0, -1, or 1. Not included ingetFieldsbecause it's redundant. Not accepted bywithorfrombecause of potential conflicts. - 5.3
Duration.prototype.abs()- if negative, reverse the sign
6. Non-Duration plus and minus methods should accept negative durations
- 6.1 If
plusis passed a negative duration, then the implementation should treat it as if the user had calledminuson the equivalent positive duration. - 6.2 If
minusis passed a negative duration, then the implementation should treat it as if the user had calledpluson the equivalent positive duration. - 6.3 This simple reversal means that the work to implement negative durations in the spec and polyfill should be mostly confined to the Duration type and its corresponding abstract operations, and won't require many changes to other types.
- 6.4 Because Duration's
plusandminusmethods can now perform addition or subtraction according to the sign of the duration, both methods must now accept identical options. Currentlyplususes'constrain'(default) and'reject'whileminususes'balanceConstrain'(default) and'balance'. To align these and to retain consistency with other Temporal uses, we'll rename'balanceConstrain'to'constrain'. Both methods will now accept'constrain'(default),'reject', or'balance'. - 6.5 A side effect of 6.4 is that a
balanceoption will now be supported for addition operations.
7. Order of operations should not be affected by negative durations
- 7.1 Addition with a negative duration should use the order of operations for subtraction with a positive duration. And vice versa: subtraction of a negative duration should use order-of-operations for addition.
- 7.2 Therefore, adding negative durations shouldn't change how order of operations is implemented in Temporal.
- 7.3 That said, there is an open issue about whether our current subtraction OOO conflicts with RFC 5545. This is a question for the calconnect group at IETF that owns the RFC 5545 spec and its descendants. Its resolution should not block negative durations; they're orthogonal issues.
8. No change to Duration.prototype.toLocaleString; continue passthrough to Intl.DurationFormat
- 8.1 IMHO,
toLocaleStringshould follow the conventions already used byIntl.RelativeTimeFormat, where both negative and positive values are accepted. Examples from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat
const rtf1 = new Intl.RelativeTimeFormat('en', { style: 'narrow' });
console.log(rtf1.format(3, 'quarter'));
//expected output: "in 3 qtrs."
console.log(rtf1.format(-1, 'day'));
//expected output: "1 day ago"- 8.2 Note that there are (at least) three possible i18n-ized formats possible for durations:
- 8.2.1 Non-relative / absolute value - e.g. "1 day and 12 hours". It's not obvious whether a negative duration should throw or should be shown using the absolute value.
- 8.2.2 Relative to now - e.g. "1 day and 12 hours ago" or "in 1 day and 12 hours". This is the format currently used by
Intl.RelativeTimeFormat. - 8.2.3 Relative-to-something - e.g. "1 day and 12 hours before". This format is not provided by
Intl.RelativeTimeFormattoday.
- 8.3. All that said, IMHO it should be up to
Intl.DurationFormatto decide both whether to accept (or throw for) negative durations and what format options should be accepted. This means that for the purposes of this proposal, theDuration.prototype.toLocaleStringshould not change from the current implementation which simply passes the duration and options through unchanged toIntl.DurationFormat, which should decide how to display the duration (or to throw if that's decided).