-
Notifications
You must be signed in to change notification settings - Fork 223
Implement date arithmetic according to Temporal specification #7012
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
base: main
Are you sure you want to change the base?
Conversation
I found some cases in the Hebrew calendar that I need to fix by fixing tc39/proposal-intl-era-monthcode#69. (This is why we have spec: so we can decide on the right thing upstream) |
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.
Generally a fan of the approach here. Will do full review on Monday when I can actually compare with the spec.
components/calendar/src/options.rs
Outdated
/// Options for taking the difference between two dates. | ||
#[derive(Copy, Clone, PartialEq, Debug, Default)] | ||
#[non_exhaustive] | ||
pub struct DateUntilOptions { |
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.
bikeshed: Until or Difference, for this and related names?
I don't mind Until but it'll be confusing if we ever add since()
Not a blocker, worth discussing separately
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.
We should add since
on Date
, doesn't need to be on the trait
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.
Absolutely, but if we do, does it make sense for the error and Options and other public API bits to be called Until?
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.
I'm fine calling the struct DateDifferenceOptions
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.
Slight preference for that (and on the trait associated type, and elsewhere). Followup ok, and if people prefer Until that's fine too.
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.
I renamed it to DateDifferenceOptions
, and also UntilError
to DifferenceError
components/calendar/src/duration.rs
Outdated
#[derive(Copy, Clone, PartialEq, Debug, Default)] | ||
#[non_exhaustive] | ||
pub struct DateUntilOptions { | ||
pub largest_unit: Option<DateDurationUnit>, |
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.
Does this need to be optional?
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.
Good point, the default value is contextually set by Temporal but I don't think we have all that context here and I think this is an ok thing to require from users.
But for convenience perhaps Unit or UntilOptions should have a default value? It's not fun to work with non exhaustives without one.
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.
the question is does None have a behaviour that is different from all values?
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.
We've previously used None as a way of distinguishing user intent, so while None may have the same behavior as an option, we might change it later or add other magical defaults.
In some APIs we have None and Auto and that's intentional.
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.
Options bags in ICU4X take Option
s by convention in ICU4X. In this particular case, I don't think there's a difference between None
and Some(default)
, but I'd rather stick with what we do elsewhere
components/calendar/src/options.rs
Outdated
/// Options for taking the difference between two dates. | ||
#[derive(Copy, Clone, PartialEq, Debug, Default)] | ||
#[non_exhaustive] | ||
pub struct DateUntilOptions { |
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.
We should add since
on Date
, doesn't need to be on the trait
With Robert's review and my partial review I'm comfortable landing this so you can start optimizing, I'll let you know if there are post merge review issues. And if there are test failures in V8. |
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.
Looks good. Slight preference for landing this and iterating so that upstream temporal_rs work can start sooner.
Main issue is that I would like to see more comments that explain the code without relying on the spec (we should still keep the spec comments)
} | ||
|
||
impl<C: CalendarArithmetic> ArithmeticDate<C> { | ||
/// Implements BalanceNonISODate |
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.
nit: spec link, and an in-words explanation. Balance is Temporal-internal jargon, we should explain what this does in non-Temporal words too
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.
I can't link to the spec because tc39/proposal-intl-era-monthcode#69 still isn't merged, but I can explain what it does
// 1. Set _resolvedMonth_ to _resolvedMonth_ - 1. | ||
// 1. If _resolvedMonth_ is 0, then | ||
resolved_month -= 1; | ||
if resolved_month == 0 { |
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.
thought: it may be possible to write this code more efficiently as DateBalancer {y, m, d, m_in_y, d_in_m}
with an add_years()
add_months()
add_days()
set of methods.
But then we don't retain the spec correspondence
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.
Yeah I want to follow up with a PR after the line-for-line code is landed to implement optimizations like this
Self::new_unchecked(resolved_year, resolved_month, resolved_day) | ||
} | ||
|
||
pub(crate) fn surpasses( |
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.
nit: link to temporal spec if possible, and doc comment explaining this in non-temporal terms
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.
Done
// 1. Let _parts_ be CalendarISOToDate(_calendar_, _fromIsoDate_). | ||
// 1. Let _y0_ be _parts_.[[Year]] + _years_. | ||
let y0 = cal.year_info_from_extended(duration.add_years_to(self.year.to_extended_year())); | ||
// 1. Let _m0_ be MonthCodeToOrdinal(_calendar_, _y0_, ! ConstrainMonthCode(_calendar_, _y0_, _parts_.[[MonthCode]], ~constrain~)). |
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.
nit: include an explanatory comment somewhere saying what this does ("transfer the month code to the new year" or something)
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.
I think for both this function and the one above, prose explanations of the various sections would be good. Matching the spec is nice for ensuring correctness, but if some behavior needs to be debugged it's good to have explanatory comments.
Prefer followup.
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.
Will do in a follow-up
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] | ||
#[allow(clippy::exhaustive_structs)] // spec-defined in Temporal | ||
#[doc(hidden)] // unstable | ||
pub struct DateDuration { |
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.
n.b. this is still hidden, as are add()
and until()
.
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.
This PR is to implement arithmetic according to Temporal. I want to decouple it from the graduation PR.
/// Error returned when comparing two [`Date`]s with [`AnyCalendar`]. | ||
#[derive(Debug, Copy, Clone)] | ||
#[non_exhaustive] | ||
pub enum AnyCalendarUntilError { |
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.
thought: maybe this should go in mod error
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.
I'll consider this as part of #7010
f1e4dbd
impl Calendar for AnyCalendar { | ||
type DateInner = AnyDateInner; | ||
type Year = YearInfo; | ||
type UntilError = AnyCalendarUntilError; |
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.
issue: I think this should go on a new trait, not calendar. AnyCalendar is the only calendar where the date difference is fallible, but this makes all calendars fallible. we can impl Date::since
infallibly for a new trait CalendarDiff
, and implement it fallibly as Date::try_since
for Date<AnyCalendar>
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.
It doesn't make all calendars infallible, since the other calendars return an UntilError = Infallible, right?
I think being able to abstract over this is good.
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.
but you still get a Result
that you have to unwrap, unwrap_infallible
doesn't exist, and even when it does, is not an ergonomic API.
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.
actually the docs currently say "In case the specific calendar objects differ on data, the data for the first calendar is used, and date2
may be converted if necessary.". Why don't we do that and make the AnyCalendar method infallible?
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.
alternatively something like this to at least not make the Date
API use infallible:
impl<A: AsCalendar> Date<A>
where
A::Calendar: Calendar<UntilError = core::convert::Infallible>,
{
pub fn until<B: AsCalendar<Calendar = A::Calendar>>(
&self,
other: &Date<B>,
options: DateUntilOptions,
) -> DateDuration {
self.calendar
.as_calendar()
.until(self.inner(), other.inner(), options)
.unwrap_or_else(|e| match e {})
}
}
impl<A: AsCalendar<Calendar = AnyCalendar>> Date<A> {
pub fn try_until<B: AsCalendar<Calendar = A::Calendar>>(
&self,
other: &Date<B>,
options: DateUntilOptions,
) -> Result<DateDuration, AnyCalendarUntilError> {
self.calendar
.as_calendar()
.until(self.inner(), other.inner(), options)
}
}
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.
I'm open to tweaking the APIs on Date
as @robertbastian suggested.
If we do that, then should we try for consistency with the add
functions?
Also, I want to note that I'm not intending to go for ergonomics in this PR. There is a lot of room we have to improve ergonomics, and I consider that very explicitly out of scope. I want to focus on landing the minimal usable API here. I'm open to prefixing functions by try_*
if it gives us more room to improve in the future.
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.
I prefer having one single Date API, even if the Result is a little unergonomic.
I'm also okay with having this never error because AnyCalendar converts.
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.
I'm not auto-converting in AnyCalendar any more. I think it's a big footgun.
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.
In that case I think I prefer having Result everywhere. let Ok(..)
is fine. Rust has Infallible
errors in a lot of places.
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.
I renamed the Date functions to be try_added_with_options
, try_add_with_options
, and try_until_with_options
. I think these names are very specific and give us room to add nicer ones in a follow-up.
I fixed the Hebrew bug; fortunately it was just a typo where I was using the wrong I think this is ready to land. The APIs are still doc(hidden). I want follow-ups to:
|
|
#1174
Fixes #3147
This implementation aligns line-for-line with the spec. I intend to go back and add fast paths (like adding multiple years or months at once), but let's check in something that we know to be spec-compliant first.