-
Notifications
You must be signed in to change notification settings - Fork 158
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
Discussion thread for resolution of weeks rounding bug #2728
Comments
First of all, here are links to the prior discussions I've been able to find on this issue.
What I can distill from the archive dive is these invariants:
So, I think invariant (1) was buggy, and we have now fixed it. (Buggy because it essentially boiled down to doing date+duration addition in the order years, months, days, weeks - wrong according to RFC5545/iCalendar. It doesn't matter in the ISO calendar where weeks are always 7 days, but does matter very much in any calendar where week length can vary, which is a case that we do accommodate.) That leaves (2) as the invariant that we need to uphold going forward. There's a third invariant that's maybe overly obvious:
|
Next, here are the current rounding results for each of the 3 cases that Richard identified, with various In each test I've used a Case 1: "months + big weeks" P1M9W15D
*Before bugfix in #2722, this would have been P1M9W15D, unchanged from the input. All other results remain the same. Case 2: "months + zero weeks" P3M12D
The bugfix in #2722 didn't change any results in this table. Case 3: "zero years + zero months + big weeks" P52W20D
*Before bugfix in #2722, this would have been P52W20D, unchanged from the input. All other results remain the same. Created with this script/* eslint-disable no-console */
import * as Temporal from './lib/temporal.mjs';
function formatRounded(d, largestUnit, smallestUnit) {
const relativeTo = Temporal.PlainDate.from('2023-06-01');
const roundingMode = 'floor';
const options = { relativeTo, roundingMode };
return d
.round({ ...options, largestUnit, smallestUnit })
.toString()
.padEnd(8);
}
function buildTable(d) {
const yy = formatRounded(d, 'years', 'years');
const ym = formatRounded(d, 'years', 'months');
const yw = formatRounded(d, 'years', 'weeks');
const yn = formatRounded(d, 'years', undefined);
const mm = formatRounded(d, 'months', 'months');
const mw = formatRounded(d, 'months', 'weeks');
const mn = formatRounded(d, 'months', undefined);
const ww = formatRounded(d, 'weeks', 'weeks');
const wn = formatRounded(d, 'weeks', undefined);
console.log(`\
| largest → smallest ↓ | years | months | weeks |
| -------------------- | -------- | -------- | -------- |
| years | ${yy } | - | - |
| months | ${ym } | ${mm } | - |
| weeks | ${yw } | ${mw } | ${ww } |
| none | ${yn } | ${mn } | ${wn } |
`);
}
console.log('\n=== Case 1: months + big weeks ===');
const d1 = Temporal.Duration.from({ months: 1, weeks: 9, days: 15 });
buildTable(d1);
console.log('\n== Case 2: months + zero weeks ===');
const d2 = Temporal.Duration.from({ months: 3, days: 12 });
buildTable(d2);
console.log('\n=== Case 3: zero years + zero months + big weeks ===');
const d3 = Temporal.Duration.from({ weeks: 52, days: 20 });
buildTable(d3); QuestionDo you see any results in this table that you find weird? Which ones? |
Next, a deeper dive on the current algorithm. Rounding the calendar units part of a duration essentially takes three steps. (Skip the parts marked "Details" unless you really want to read them. They are simplified prose descriptions of the spec algorithms.)
Now I want to take a look at how this works in practice for an example duration and an example rounding operation for each of the three cases that Richard identified. These are the same example durations as in the tables above. Case 1: "months + big weeks" P1M9W15D rounded to smallestUnit weeksd = Temporal.Duration.from({ months: 1, weeks: 9, days: 15 });
d.round({ smallestUnit: 'weeks', relativeTo: '2023-06-01', roundingMode: 'floor' })
Case 2: "months + zero weeks" P3M12D rounded to smallestUnit weeksd = Temporal.Duration.from({ months: 3, days: 12 });
d.round({ smallestUnit: 'weeks', relativeTo: '2023-06-01', roundingMode: 'floor' })
Case 3: "zero years + zero months + big weeks" P52W20D rounded to largestUnit years, smallestUnit weeksd = Temporal.Duration.from({ weeks: 52, days: 20 });
d.round({ smallestUnit: 'weeks', largestUnit: 'years', relativeTo: '2023-06-01', roundingMode: 'floor' })
|
Lots of exploration to report here! Next thing I'll talk about is alternative algorithms for rounding to weeks. Here's the one that was discussed during the champions meeting:
I don't think we should do this "double rounding". For each of the 3 example durations from the previous comment, I'll show how it produces results which I think are worse than the status quo: Case 1: "months + big weeks" P1M9W15D rounded to smallestUnit weeks
Case 2: "months + zero weeks" P3M12D rounded to smallestUnit weeks
Case 3: "zero years + zero months + big weeks" P52W20D rounded to largestUnit years, smallestUnit weeks
In each of these cases, converting the weeks in the post-rounding balance step results in a remainder of days, which needs to be rounded again. Normally I'd say rounding to the nearest week up or down should give you a duration that's no more than 6 days longer or shorter than the input duration, but here you can get up to 12 days! (This is why I'm illustrating the algorithm with This is even worse if Other alternativesI tried to think of some other alternatives, but I concluded that any algorithm that balances weeks post-rounding is going to have the same problem. For example, this one would avoid balancing all the calendar units down in the first step:
This algorithm happens to work well on the duration in Case 1, for example, but you can still get a remainder of days in step 3 and therefore double-rounding. For example, if the input duration is P1M9W28D and
QuestionAny ideas for alternative algorithms that don't double-round? |
Hi there, spectator here. I am just a member of the peanut gallery who doesn't have the full picture of this project. But I've been fascinated by this problem, and, if I may, I would like to propose an alternative algorithm for your consideration. It is a refinement of the flawed algorithm proposed at the end of your analysis above:
By definition of Step 1, If In all other cases, exactly one of If I am not mistaken, this algorithm should:
Unless I'm missing something, I believe that satisfies all of your requirements. I am unfortunately not familiar enough with your project to be able to discern whether this algorithm breaks any of your other invariants, nor am I bright enough to discern whether this algorithm is congruent to one of the already proposed algorithms. I just figured I'd offer up the algorithm that seemed obvious to me after not seeing it proposed here thus far, just in case it was missed. |
Thanks for writing that up! I'll take a look soon at how it might work in the context of Temporal.Duration. |
This discussion has effectively become obsolete by what we are proposing in #2758, namely that the following two lines should always be equivalent to each other: past.until(past.add(duration), { largestUnit, smallestUnit, roundingMode, etc })
duration.round({ relativeTo: past, largestUnit, smallestUnit, roundingMode, etc }) So we will by definition always use the same algorithm as until/since already uses. This produces the following results for the cases that I investigated above: Case 1: months + big weeks
Case 2: months + zero weeks
Case 3: zero years + zero months + big weeks
Note in case 1, P3M2W instead of P1M11W, and in case 3, P1Y2W or P12M2W instead of 54W. This seems to address the concerns that people had about results not seeming natural. @DiamondIceNS Thanks so much for proposing the alternative algorithm. I'm sorry it took so long to follow up on it — I've been busy resolving other issues with this proposal. |
No concerns here. Bigger fish gotta fry first. 👍 You've been crushing it on the issues lately. I take it a lot of co-dependent issues are finally cascading? Keep up the amazing work, looking forward to Temporal being a native part of JavaScript soon! |
Normative PR #2722 resolved a bug with rounding durations to a largestUnit or smallestUnit of weeks. In the champions meeting of 2023-11-16 we agreed to discuss after merging and either affirm that this was the correct fix, or adjust it in a follow-up.
In the meeting, @gibson042 identified 3 interesting cases:
I've done some analysis of these three cases and will summarize it in this thread.
The text was updated successfully, but these errors were encountered: