-
Notifications
You must be signed in to change notification settings - Fork 146
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
difference
should always return a "top-heavy balanced" duration with largest-first order of operations
#993
Comments
I put this in the "design decisions" milestone because it may be blocking resolution of #913. |
@pipobscure, @gibson042 - As this may block resolving #913, I'm pinging you here. What would you expect the value of |
Temporal.Date.from('2020-03-30').difference(Temporal.Date.from('2020-01-31'), { largestUnit: 'months' });
// => P59D, but expected: P1M???D (??? = some number of days)
Temporal.Date.from('2020-03-31').difference(Temporal.Date.from('2020-01-31'), { largestUnit: 'months' });
// => P2M I honestly can't come up with a good value for the Looking around, Moment returns P1M28D (actually P1M27DT23H |
Java returns
LocalDate d1 = LocalDate.of(2020, 1, 31);
LocalDate d2 = LocalDate.of(2020, 3, 30);
Period period = Period.between(d1, d2);
System.out.println("diff: " + period.toString());
// => P1M30D .NET's |
Here's another interesting result from Java: it's not reversible in all cases. And I'm unsure my guess at the algorithm above is correct. // NOT REVERSIBLE
LocalDate d1 = LocalDate.of(2019, 1, 29);
LocalDate d2 = LocalDate.of(2019, 3, 1);
Period period = Period.between(d1, d2);
System.out.println("diff: " + period.toString());
// => P1M1D
Period period2 = Period.between(d2, d1);
System.out.println("diff: " + period2.toString());
// => P-1M-3D
// NOT REVERSIBLE
LocalDate d1 = LocalDate.of(2019, 1, 30);
LocalDate d2 = LocalDate.of(2019, 3, 29);
Period period = Period.between(d1, d2);
System.out.println("diff: " + period.toString());
// => P1M29D
Period period2 = Period.between(d2, d1);
System.out.println("diff: " + period2.toString());
// => P-1M-30D
// REVERSIBLE
LocalDate d1 = LocalDate.of(2019, 1, 31);
LocalDate d2 = LocalDate.of(2019, 3, 30);
Period period = Period.between(d1, d2);
System.out.println("diff: " + period.toString());
// => P1M30D
Period period2 = Period.between(d2, d1);
System.out.println("diff: " + period2.toString());
// => P-1M-30D Here's what the documentation of
Translating this algorithm into Temporal pseudocode, I suspect that it provides the following guarantee: let diff = date2.difference(date1, {largestUnit: 'months'});
date1.add(diff).equals(date2); // always true But it probably DOESN'T always provide these guarantees: // subtracting from endpoint may not yield the same result as adding to the starting point
date2.subtract(diff).equals(date1); // not always true
// reversing the arguments may not return the same result
let negative = date1.difference(date2, {largestUnit: 'months'});
diff.negated().equals(negative); // not always true It seems like there are at least two levers we can play with in a difference algorithm:
It'd be interesting to see if either of these levers would enable the additional reversibility assertions above. That said, I'm not exactly sure how reversing the OOO would work for And honestly I'm not sure how critical these reversibility guarantees are. Seems like being clear about what's expected to work and what's not might be enough.
I don't agree. If the user's intent was to get a zero-months duration, they would have used the default
What's the calculation you have in mind that gets to 30 or 28? |
But the result cannot have nonzero months without data loss.
P1M30D would come from largest unit to smallest unit calculations with intermediate constraining (the calculation you described above): 2020-01-31 to 2020-03-30 is ≥P1M <P2M, [2020-01-31 + P1M = 2020-02-31 →] 2020-02-29 to 2020-03-30 is P30D, the result is therefore P1M30D. P1M28D would come from largest unit to smallest unit calculations with intermediate balancing: 2020-01-31 to 2020-03-30 is ≥P1M <P2M, [2020-01-31 + P1M = 2020-02-31 →] 2020-03-02 to 2020-03-30 is P28D, the result is therefore P1M28D. |
Meeting 2020-10-15:
d = a.difference(b, {largestUnits: 'months'});
b.add(d).equals(a); // will always be true, regardless of values of `a` and `b`
a.subtract(d).equals(b); // may not be true for some values of `a` and `b`
// if there's rounding, then it's not reversible!
dRounded = a.difference(b, {largestUnits: 'months', smallestUnit: 'days'});
b.add(dRounded).equals(a); // may not be true, depending on `a` and `b`
const d = thisValue.difference(otherValue, {largestUnits: 'months'});
equal (d.toString(), expected.toString());
equal (otherValue.add(d).toString(), thisValue.toString());
|
Are you sure about all the test cases?
|
You're right. I edited this test case above.
My original test case was wrong, but I believe that this result should be -P1M28D, not -P1M29D:
I edited accordingly. BTW, I got both of those cases above wrong because I was assuming that Java's algorithm matches what we proposed. However, this is not the case. Java is using a different calculation methods depending on whether the difference is being calculated from larger to smaller vs. smaller to larger. I think our proposed method is better because it's more predictable. |
difference
should always return a "top-heavy balanced" duration with largest-first order of operations
I added some additional clarifying content to the proposal above to incorporate what I learned from after implementing this change in ZoneDateTime:
|
If it's the case that this change makes |
@pipobscure Have you been able to make any progress on this? |
Not enough and I won’t get back to it before the weekend. So if you think you’ll be quicker, please!!! |
I'm almost done with a PR. One question: in A good way to understand the choice is to choose which of the comparisons below should always be true. Only one can always be true for months and years d = a.since(b, {largestUnit: 'years'));
a.subtract(d).equals(b) === true; // if `relativeTo` is `this`
b.add(d).equals(a) === true; // if `relativeTo` is `other` A) Arguments in favor of
B) Arguments in favor of
My weakly-held opinion is that arguments in favor of (B) are somewhat more compelling, because the refactoring issue seems significant and otherwise they both seem fairly evenly matched. Therefore, unless anyone objects, I'll plan to implement if it matters, I believe that the current polyfill is a mixed implementation: rounding in @pipobscure @gibson042 @ptomato - FYI EDIT - I updated this post to clarify the proposed options. |
I think it would be unexpected and weird if one method started from its receiver but the other method started from it argument. Having As for whether the starting point should be the receiver for both methods or the argument for both methods, I don't have any intuition and my only opinion is a preference for whichever results in the most straightforward code for use cases we've identified (e.g., in the cookbook). |
We already decided on "A" for rounding;
Given that decision, in my opinion there is only one self-consistent option, which is to pick "A" consistently, so that |
OK I implemented (A) in #1123. That PR is ready for review. |
difference
is always supposed to return a "top-heavy balanced" duration, meaning it should be balanced except possibly forlargestUnit
. But that's not happening in the example below.The text was updated successfully, but these errors were encountered: