Skip to content

Proposal: Negative Durations #782

@justingrant

Description

@justingrant

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'} in with() and from() of non-Duration types #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. -P2D means a negative 2-day duration, while P2D and +P2D both 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.toString will 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.from will 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 Duration fields (property getters, getFields, from, and with) 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 sign field. The main problem with this approach is that it makes with seem ambiguous. If you have a duration -P2D and 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:
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's plus or minus method) should throw if any of the non-zero input units have different signs.
  • 4.3 The Duration constructor must throw if it's passed non-zero units with different signs.
  • 4.4 Duration.with can 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})`; // throws

5. 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 in getFields because it's redundant. Not accepted by with or from because 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 plus is passed a negative duration, then the implementation should treat it as if the user had called minus on the equivalent positive duration.
  • 6.2 If minus is passed a negative duration, then the implementation should treat it as if the user had called plus on 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 plus and minus methods can now perform addition or subtraction according to the sign of the duration, both methods must now accept identical options. Currently plus uses 'constrain' (default) and 'reject' while minus uses '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 balance option 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

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.RelativeTimeFormat today.
  • 8.3. All that said, IMHO it should be up to Intl.DurationFormat to 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, the Duration.prototype.toLocaleString should not change from the current implementation which simply passes the duration and options through unchanged to Intl.DurationFormat, which should decide how to display the duration (or to throw if that's decided).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions