-
Notifications
You must be signed in to change notification settings - Fork 156
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
Normative: Fix Duration rounding relative to ZonedDateTime #2758
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2758 +/- ##
==========================================
- Coverage 96.51% 96.49% -0.02%
==========================================
Files 23 23
Lines 12386 12501 +115
Branches 2294 2257 -37
==========================================
+ Hits 11954 12063 +109
- Misses 372 380 +8
+ Partials 60 58 -2 ☔ View full report in Codecov by Sentry. |
Thanks @ptomato, this successfully addresses my concerns brought up in #2742 (comment) I've verified it succeeds under my original test case... const past = Temporal.ZonedDateTime.from("2019-11-01T00:00:00-07:00[America/Vancouver]")
const duration = Temporal.Duration.from({ years: 1, hours: 24 })
const future = past.add(duration) // 2020-11-01T23:00:00-08:00[America/Vancouver]
duration.round({ largestUnit: 'years', relativeTo: past }).toString() // P1YT24H -- CORRECT ...as well as succeeding in some others... const past = Temporal.ZonedDateTime.from("2019-11-01T00:00:00-07:00[America/Vancouver]")
const duration = Temporal.Duration.from({ days: 366, hours: 24 })
const future = past.add(duration) // 2020-11-01T23:00:00-08:00[America/Vancouver]
duration.round({ largestUnit: 'years', relativeTo: past }).toString() // P1YT24H -- CORRECT const past = Temporal.ZonedDateTime.from("2020-10-31T00:00:00-07:00[America/Vancouver]")
const duration = Temporal.Duration.from({ days: 1, hours: 24 })
const future = past.add(duration) // 2020-11-01T23:00:00-08:00[America/Vancouver]
duration.round({ largestUnit: 'years', relativeTo: past }).toString() // P1YT24H -- CORRECT You said in #2742 (comment) you'd like to consolidate the code path for round() and until(). FWIW, temporal-polyfill achieves this by creating an |
b184abd
to
36c5d63
Compare
This now includes a second commit with the consolidation of the code paths for round(), total(), and until(). I'll still be auditing this before presenting to TC39 to make sure results don't change unintentionally, and to minimize unnecessary calls into user code. If that will pose a problem reviewing the materials for TC39 plenary, please let me know. |
36c5d63
to
cf03c63
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM so far
cf03c63
to
b3985a4
Compare
Looks great to me. Love this round/total/until consolidation. This consolidation fixes the bug I brought up here: Temporal.Duration.from({ months: 1, days: 15, hours: 12 }).total({
unit: 'month',
relativeTo: '2024-02-10T02:00[America/New_York]'
})
// expected: 1.4986559139784945 |
b3985a4
to
62a09c6
Compare
I've rebased this and brought it up to date. Tests are in tc39/test262#4041. May still change to address some comments from @arshaw. |
7f8b808
to
30ef811
Compare
df63308
to
2845e80
Compare
5fb47aa
to
8bb21a8
Compare
When the time portion of a duration, rounded relative to a ZonedDateTime, would land on a non-24-hour day, we'd get incorrect results. This is a regression from #2508 where although switching the order of BalanceDateDurationRelative and BalanceTimeDurationRelative was correct, we should not have removed the MoveRelativeZonedDateTime call. Closes: #2742
f6ad808
to
39b8616
Compare
This is ready now, addressing the opportunities for optimizing the new shared code path that @arshaw identified. Tests are in tc39/test262#4041. Review appreciated before merging. |
This comment was marked as resolved.
This comment was marked as resolved.
^^^ @ptomato, nevermind, all tests are passing. reviewing now... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Things look fantastic @ptomato! The only potential bug I see is with deltaDays
at the end of RoundRelativeDuration
. All my other feedback should be considered editorial.
In the future, we may want to detangle the total()
code path from the RoundRelativeDuration
as evident from the code-smelly total: NaN
in NudgeToZonedTime
.
There might be an opportunity to give a formal shape to "relativeTo". Kevin Ness brought this up in the last meeting regarding difficulty in statically typed languages.
type RelativeTo = {
plainDateTime: Temporal.PlainDateTime
timeZone: Temporal.TimeZone // or null if UTC?
}
function normalizeRelativeTo(input): RelativeTo {
if (input instanceof Temporal.ZonedDateTime) {
return {
plainDateTime: input.toPlainDateTime(),
timeZone: input.getTimeZone(),
}
} else if (input instanceof Temporal.PlainDate) {
return {
plainDateTime: input.toPlainDateTime(), // gives 00:00:00
timeZone: new Temporal.TimeZone('UTC')
}
}
}
/*
Adds a Duration to the relativeTo and returns an epoch nanoseconds
This function is only ever called with time units zeroed out!
(It's just the way our nudging/bubbling system works during relative-rounding)
Thus, we don't need to worry about the intricacies of *when* time units are
added in PlainDateTime vs ZonedDateTime.
A utility function is nice because we don't need to call GetUTCEpochNanoseconds and
GetInstantFor conditionally each time.
*/
function addToRelativeTo(relativeTo: RelativeTo, duration): bigint {
return relativeTo.timeZone.getInstantFor(
relativeTo.plainDateTime.add(duration)
).epochNanoseconds
}
Thanks
39b8616
to
510d53f
Compare
These optimizations were developed by Adam Shaw: https://gist.github.com/arshaw/36d3152c21482bcb78ea2c69591b20e0 It does the same thing as previously, although fixes some incidental edge cases that Adam discovered. However, the algorithm is simpler to explain and to understand, and also makes fewer calls into user code. It uses three variations on a bounding technique for rounding: computing the upper and lower result, and checking which one is closer to the original duration, and 'nudging' the duration up or down accordingly. There is one variation for calendar units, one variation for rounding relative to a ZonedDateTime where smallestUnit is a time unit and largestUnit is a calendar unit, and one variation for time units. RoundDuration becomes a lot more simplified, any part of it that was complex is now split out into the new RoundRelativeDuration and BubbleRelativeDuration operations, and the three 'nudging' operations. The operations NormalizedTimeDurationToDays, BalanceTimeDurationRelative, BalanceDateDurationRelative, MoveRelativeDate, MoveRelativeZonedDateTime, and AdjustRoundedDurationDays are no longer needed. Their functionality is subsumed by the new operations. Closes: #2792 Closes: #2817
510d53f
to
4bf121c
Compare
OK, with the bug fixed and an extra test case added, I'm hoping this is ready to merge now. I'll plan to land it as soon as tc39/test262#4041 gets the green light. |
Ok great, glad everything worked out. Things look good from my POV. |
OK, last outstanding change approved in February 2024 TC39 meeting, tests are merged, all review comments addressed. Thanks for the help everyone. |
In #2758 we kept the code paths for Duration.p.round() and Duration.p.total() the same. However, it is actually simpler to separate them in most places, except for NudgeToCalendarUnit. - Splits TotalRelativeDuration out of RoundRelativeDuration - Splits TotalTimeDuration out of RoundTimeDuration - Splits DifferenceZonedDateTimeWithTotal out of DifferenceZonedDateTimeWithRounding - Splits DifferencePlainDateTimeWithTotal out of DifferencePlainDateTimeWithRounding - Removes [[Total]] field from Nudge Result Record - Changes NudgeToCalendarUnit to return a record of { [[NudgeResult]], [[Total]] } Closes: #2947
In #2758 we kept the code paths for Duration.p.round() and Duration.p.total() the same. However, it is actually simpler to separate them in most places, except for NudgeToCalendarUnit. - Splits TotalRelativeDuration out of RoundRelativeDuration - Splits TotalTimeDuration out of RoundTimeDuration - Splits DifferenceZonedDateTimeWithTotal out of DifferenceZonedDateTimeWithRounding - Splits DifferencePlainDateTimeWithTotal out of DifferencePlainDateTimeWithRounding - Removes [[Total]] field from Nudge Result Record - Changes NudgeToCalendarUnit to return a record of { [[NudgeResult]], [[Total]] } Closes: #2947
When the time portion of a duration, rounded relative to a ZonedDateTime, would land on a non-24-hour day, we'd get incorrect results. This is a regression from #2508 where although switching the order of BalanceDateDurationRelative and BalanceTimeDurationRelative was correct, we should not have removed the MoveRelativeZonedDateTime call.
Closes: #2742