Proposal-temporal: Rename `difference` to communicate directionality

Created on 15 Oct 2020  Â·  9Comments  Â·  Source: tc39/proposal-temporal

difference does a good job of communicating that the returned value is a Duration instance, but a very poor job of communicating the meaning of its sign.

// How many days are left in this hell of a year?!?
let now = Temporal.now.date();
let remaining = now.with({year: today.year + 1, month: 1, day: 1}).difference(now, {largestUnit: "days"});
// The magnitude of `remaining` must be reduced by one day, but that looks different based on its sign.
// Authors (and maintainers!) need to check if difference is "this minus that" or "that minus this".
remaining = remaining.subtract({days: 1});

// Given two Temporal instance values of unknown specific type, which is greater?
let sign = x1.difference(x2).sign;
// Authors (and maintainers!) need to check if difference is "this minus that" or "that minus this".
let x1IsLater = sign > 0;

This situation would be much better if difference were replaced with a method that clearly communicated directionality.

// How many days are left in this hell of a year?!?
let now = Temporal.now.date();
let remaining = now.with({year: today.year + 1, month: 1, day: 1}).since(now, {largestUnit: "days"});
// `remaining` is obviously nonnegative, so there's an obvious way to reduce it by one day.
remaining = remaining.subtract({days: 1});

// Given two Temporal instance values of unknown specific type, which is greater?
let sign = x1.since(x2).sign;
// `sign` is positive if and only if x1 is later than x2.
let x1IsLater = sign > 0;

I like since or durationSince, but the polarity could also be inverted (i.e., "that minus this") with something like until or durationUntil.

bikeshed documentation ergonomics polyfill spec-text

Most helpful comment

By happy accident I found I had written this code in one of the cookbook recipes:

const duration = Temporal.Instant.from('2020-04-01T13:00-07:00[America/Los_Angeles]').difference(Temporal.now.instant());
`It's ${duration.toLocaleString()} ${duration.sign < 0 ? 'until' : 'since'} the TC39 Temporal presentation`;

All 9 comments

Personally, since is still a bit confusing when your use-case is to just check if an instance is "later" or "earlier" than another.

Mimmicking the java.time library, I'd propose two new methods for this specific usecase:

let x1IsLater = x1.after(x2) // Or maybe x1.isAfter(x2)
let x1IsEarlier = x1.before(x2) // Or maybe x1.isBefore(x2)

while these methods are very similar and somewhat redundant, they an added benefit: since().sign > 0 is hard to parse for a human reader, but .after is much easier to read.

I don't mean to remove the difference/since method, but I feel like this would be an improvement over the current API for this specific(and very common) use case. difference/ since would still be useful to know exactly _how long_ is has been from one temporal to another.

Note that, considering the redundancy of methods, Temporal.Instant.compare also serves the purpose of determining wether a temporal instance is before or after another temporal instant; that is, Temporal.Instant.compare(x1,x2) has the same value as x1.difference(x2).sign. Since we already have these two "redundant" forms of comparison, IMO new methods with the purpose of legibility are worth it.

FWIW, Java uses until for this operation on its equivalent of the PlainDate type:
https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html#until-java.time.chrono.ChronoLocalDate-

Given that we're matching Java's terminology in other places (e.g. Instant, ZonedDateTime), if we do change the name of this method then I'd vote for until over since for this reason.

until would reverse the order of the operation: it'd start from this and go to other.

Meeting 2020-10-23:

  • Rename difference => since
  • Add until, but only if its implementation is not very difficult

By happy accident I found I had written this code in one of the cookbook recipes:

const duration = Temporal.Instant.from('2020-04-01T13:00-07:00[America/Los_Angeles]').difference(Temporal.now.instant());
`It's ${duration.toLocaleString()} ${duration.sign < 0 ? 'until' : 'since'} the TC39 Temporal presentation`;

OK, I think I've puzzled this out — in any case, Instant.until() is trivial to implement because there is no calendar involved.

For the other types, the implementation should be easy as well, although I've realized there are some bugs in the Duration rounding algorithm.

For earlier.until(later):

  • _diff_ = _later_ − _earlier_
  • _diff_ = round _diff_ relative to _earlier_
  • return _diff_

For later.since(earlier):

  • _diff_ = _later_ − _earlier_
  • _diff_ = round _−diff_ relative to _later_
  • return _−diff_

As an example with Temporal.Date, we'll use _earlier_ = 2019-01-01, _later_ = 2019-02-15, giving a _diff_ of 45 days. Let's say we want to round to the nearest month:

Temporal.Date.from('2019-02-15').since('2019-01-01', { smallestUnit: 'months' }):

  • round −45 days relative to 2019-02-15
  • one calendar month preceding 2019-02-15 is 31 days
  • add −1 month to answer
  • round -14 days relative to 2019-01-15
  • one calendar month preceding 2019-01-15 is 31 days
  • abs(−14/31) < 0.5, round down
  • answer: −1 month
  • return Temporal.Duration.from({ months: 1 })

Temporal.Date.from('2019-01-01').until('2019-02-15', { smallestUnit: 'months' }):

  • round 45 days relative to 2019-01-01
  • one calendar month starting 2019-01-01 is 31 days
  • add 1 month to answer
  • round 14 days relative to 2019-02-01
  • one calendar month starting 2019-02-01 is 28 days
  • abs(14/28) ≥ 0.5, round up and add one month to answer
  • answer: 2 months
  • return Temporal.Duration.from({ months: 2 })

The deal is that these answers should be the same as Temporal.Duration.from({ days: -45 }).round({ relativeTo: '2019-02-15', smallestUnit: 'months' }) and Temporal.Duration.from({ days: 45 }).round({ relativeTo: '2019-01-01', smallestUnit: 'months' }) respectively, and that is not currently the case.

I will proceed by implementing the until() methods as described, and will fix the bugs in the rounding algorithm separately.

(In other words, the _relative_ point is not necessarily the _starting_ point. For since(), it's the _ending_ point.)

@ptomato - I believe your analysis is correct. BTW, I was starting to look at two things today:

  • Implementing total()
  • reproducing the potential issue we found in code review on Friday, where calendar.daysInYear(relativeTo) (or daysinMonth, etc.) is used instead of calculating the actual number of days between relativeTo and one year earlier/later.

Should I wait on these two things until your changes to the rounding algorithm are finished?

@justingrant: I think exactly that issue is one of the two problems with the rounding algorithm that need to be fixed, so no need to try to reproduce it any longer. If you have time to implement total() this week, that would be really helpful, and I don't think it needs to wait on anything. The rounding algorithm will only need to change in edge cases.

Hey @gibson042 - Kudos for pushing to make this change. In a few days of using the new names while writing test code, I'm amazed at how much easier it is to understand what each operations will do. Thanks!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

littledan picture littledan  Â·  4Comments

ptomato picture ptomato  Â·  5Comments

mj1856 picture mj1856  Â·  7Comments

Ms2ger picture Ms2ger  Â·  6Comments

justingrant picture justingrant  Â·  4Comments