Skip to content

Commit 1c5c2ad

Browse files
authored
Implement date arithmetic according to Temporal specification (#7012)
#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.
1 parent 17fbf6a commit 1c5c2ad

24 files changed

+1471
-553
lines changed

components/calendar/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,5 +66,9 @@ harness = false
6666
name = "convert"
6767
harness = false
6868

69+
[[test]]
70+
name = "arithmetic"
71+
required-features = ["ixdtf"]
72+
6973
[package.metadata.cargo-semver-checks.lints]
7074
workspace = true

components/calendar/benches/date.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,27 @@ pub struct Test {
2020
use criterion::{
2121
black_box, criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
2222
};
23-
use icu_calendar::{AsCalendar, Calendar, Date, DateDuration};
23+
use icu_calendar::{
24+
options::{DateAddOptions, Overflow},
25+
types, AsCalendar, Calendar, Date,
26+
};
2427

2528
fn bench_date<A: AsCalendar>(date: &mut Date<A>) {
2629
// black_box used to avoid compiler optimization.
2730
// Arithmetic
28-
date.add(DateDuration {
29-
is_negative: false,
30-
years: black_box(1),
31-
months: black_box(2),
32-
weeks: black_box(3),
33-
days: black_box(4),
34-
});
31+
let mut options = DateAddOptions::default();
32+
options.overflow = Some(Overflow::Constrain);
33+
date.try_add_with_options(
34+
types::DateDuration {
35+
is_negative: false,
36+
years: black_box(1),
37+
months: black_box(2),
38+
weeks: black_box(3),
39+
days: black_box(4),
40+
},
41+
options,
42+
)
43+
.unwrap();
3544

3645
// Retrieving vals
3746
let _ = black_box(date.year());

components/calendar/src/any_calendar.rs

Lines changed: 126 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use crate::cal::iso::IsoDateInner;
88
use crate::cal::*;
99
use crate::error::DateError;
1010
use crate::options::DateFromFieldsOptions;
11+
use crate::options::{DateAddOptions, DateDifferenceOptions};
1112
use crate::types::{DateFields, YearInfo};
12-
use crate::{types, AsCalendar, Calendar, Date, DateDuration, DateDurationUnit, Ref};
13+
use crate::{types, AsCalendar, Calendar, Date, Ref};
1314

1415
use crate::preferences::{CalendarAlgorithm, HijriCalendarAlgorithm};
1516
use icu_locale_core::preferences::define_preferences;
@@ -211,10 +212,45 @@ macro_rules! match_cal {
211212
};
212213
}
213214

215+
/// Error returned when comparing two [`Date`]s with [`AnyCalendar`].
216+
#[derive(Clone, Copy, PartialEq, Debug)]
217+
#[non_exhaustive]
218+
#[doc(hidden)] // unstable, not yet graduated
219+
pub enum AnyCalendarDifferenceError {
220+
/// The calendars of the two dates being compared are not equal.
221+
///
222+
/// To compare dates in different calendars, convert them to the same calendar first.
223+
///
224+
/// # Examples
225+
///
226+
/// ```
227+
/// use icu::calendar::Date;
228+
/// use icu::calendar::cal::AnyCalendarDifferenceError;
229+
///
230+
/// let d1 = Date::try_new_gregorian(2000, 1, 1).unwrap().to_any();
231+
/// let d2 = Date::try_new_hebrew(5780, 1, 1).unwrap().to_any();
232+
///
233+
/// assert!(matches!(
234+
/// d1.try_until_with_options(&d2, Default::default()),
235+
/// Err(AnyCalendarDifferenceError::MismatchedCalendars),
236+
/// ));
237+
///
238+
/// // To compare the dates, convert them to the same calendar,
239+
/// // such as ISO.
240+
///
241+
/// assert!(matches!(
242+
/// d1.to_iso().try_until_with_options(&d2.to_iso(), Default::default()),
243+
/// Ok(_)
244+
/// ));
245+
/// ```
246+
MismatchedCalendars,
247+
}
248+
214249
impl crate::cal::scaffold::UnstableSealed for AnyCalendar {}
215250
impl Calendar for AnyCalendar {
216251
type DateInner = AnyDateInner;
217252
type Year = YearInfo;
253+
type DifferenceError = AnyCalendarDifferenceError;
218254

219255
fn from_fields(
220256
&self,
@@ -252,34 +288,58 @@ impl Calendar for AnyCalendar {
252288
match_cal_and_date!(match (self, date): (c, d) => c.days_in_month(d))
253289
}
254290

255-
fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) {
256-
match (self, date) {
257-
(Self::Buddhist(c), AnyDateInner::Buddhist(ref mut d)) => c.offset_date(d, offset),
258-
(Self::Chinese(c), AnyDateInner::Chinese(ref mut d)) => c.offset_date(d, offset),
259-
(Self::Coptic(c), AnyDateInner::Coptic(ref mut d)) => c.offset_date(d, offset),
260-
(Self::Dangi(c), AnyDateInner::Dangi(ref mut d)) => c.offset_date(d, offset),
261-
(Self::Ethiopian(c), AnyDateInner::Ethiopian(ref mut d)) => c.offset_date(d, offset),
262-
(Self::Gregorian(c), AnyDateInner::Gregorian(ref mut d)) => c.offset_date(d, offset),
263-
(Self::Hebrew(c), AnyDateInner::Hebrew(ref mut d)) => c.offset_date(d, offset),
264-
(Self::Indian(c), AnyDateInner::Indian(ref mut d)) => c.offset_date(d, offset),
265-
(Self::HijriTabular(c), &mut AnyDateInner::HijriTabular(ref mut d, sighting))
266-
if c.0 == sighting =>
291+
fn add(
292+
&self,
293+
date: &Self::DateInner,
294+
duration: types::DateDuration,
295+
options: DateAddOptions,
296+
) -> Result<Self::DateInner, DateError> {
297+
let mut date = *date;
298+
match (self, &mut date) {
299+
(Self::Buddhist(c), AnyDateInner::Buddhist(ref mut d)) => {
300+
*d = c.add(d, duration, options)?
301+
}
302+
(Self::Chinese(c), AnyDateInner::Chinese(ref mut d)) => {
303+
*d = c.add(d, duration, options)?
304+
}
305+
(Self::Coptic(c), AnyDateInner::Coptic(ref mut d)) => {
306+
*d = c.add(d, duration, options)?
307+
}
308+
(Self::Dangi(c), AnyDateInner::Dangi(ref mut d)) => *d = c.add(d, duration, options)?,
309+
(Self::Ethiopian(c), AnyDateInner::Ethiopian(ref mut d)) => {
310+
*d = c.add(d, duration, options)?
311+
}
312+
(Self::Gregorian(c), AnyDateInner::Gregorian(ref mut d)) => {
313+
*d = c.add(d, duration, options)?
314+
}
315+
(Self::Hebrew(c), AnyDateInner::Hebrew(ref mut d)) => {
316+
*d = c.add(d, duration, options)?
317+
}
318+
(Self::Indian(c), AnyDateInner::Indian(ref mut d)) => {
319+
*d = c.add(d, duration, options)?
320+
}
321+
(Self::HijriTabular(c), AnyDateInner::HijriTabular(ref mut d, sighting))
322+
if c.0 == *sighting =>
267323
{
268-
c.offset_date(d, offset)
324+
*d = c.add(d, duration, options)?
269325
}
270326
(Self::HijriSimulated(c), AnyDateInner::HijriSimulated(ref mut d)) => {
271-
c.offset_date(d, offset)
327+
*d = c.add(d, duration, options)?
272328
}
273329
(Self::HijriUmmAlQura(c), AnyDateInner::HijriUmmAlQura(ref mut d)) => {
274-
c.offset_date(d, offset)
330+
*d = c.add(d, duration, options)?
331+
}
332+
(Self::Iso(c), AnyDateInner::Iso(ref mut d)) => *d = c.add(d, duration, options)?,
333+
(Self::Japanese(c), AnyDateInner::Japanese(ref mut d)) => {
334+
*d = c.add(d, duration, options)?
275335
}
276-
(Self::Iso(c), AnyDateInner::Iso(ref mut d)) => c.offset_date(d, offset),
277-
(Self::Japanese(c), AnyDateInner::Japanese(ref mut d)) => c.offset_date(d, offset),
278336
(Self::JapaneseExtended(c), AnyDateInner::JapaneseExtended(ref mut d)) => {
279-
c.offset_date(d, offset)
337+
*d = c.add(d, duration, options)?
280338
}
281-
(Self::Persian(c), AnyDateInner::Persian(ref mut d)) => c.offset_date(d, offset),
282-
(Self::Roc(c), AnyDateInner::Roc(ref mut d)) => c.offset_date(d, offset),
339+
(Self::Persian(c), AnyDateInner::Persian(ref mut d)) => {
340+
*d = c.add(d, duration, options)?
341+
}
342+
(Self::Roc(c), AnyDateInner::Roc(ref mut d)) => *d = c.add(d, duration, options)?,
283343
// This is only reached from misuse of from_raw, a semi-internal api
284344
#[expect(clippy::panic)]
285345
(_, d) => panic!(
@@ -288,121 +348,77 @@ impl Calendar for AnyCalendar {
288348
d.kind().debug_name()
289349
),
290350
}
351+
Ok(date)
291352
}
292353

293354
fn until(
294355
&self,
295356
date1: &Self::DateInner,
296357
date2: &Self::DateInner,
297-
calendar2: &Self,
298-
largest_unit: DateDurationUnit,
299-
smallest_unit: DateDurationUnit,
300-
) -> DateDuration {
301-
match (self, calendar2, date1, date2) {
302-
(
303-
Self::Buddhist(c1),
304-
Self::Buddhist(c2),
305-
AnyDateInner::Buddhist(d1),
306-
AnyDateInner::Buddhist(d2),
307-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
308-
(
309-
Self::Chinese(c1),
310-
Self::Chinese(c2),
311-
AnyDateInner::Chinese(d1),
312-
AnyDateInner::Chinese(d2),
313-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
314-
(
315-
Self::Coptic(c1),
316-
Self::Coptic(c2),
317-
AnyDateInner::Coptic(d1),
318-
AnyDateInner::Coptic(d2),
319-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
320-
(
321-
Self::Dangi(c1),
322-
Self::Dangi(c2),
323-
AnyDateInner::Dangi(d1),
324-
AnyDateInner::Dangi(d2),
325-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
326-
(
327-
Self::Ethiopian(c1),
328-
Self::Ethiopian(c2),
329-
AnyDateInner::Ethiopian(d1),
330-
AnyDateInner::Ethiopian(d2),
331-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
332-
(
333-
Self::Gregorian(c1),
334-
Self::Gregorian(c2),
335-
AnyDateInner::Gregorian(d1),
336-
AnyDateInner::Gregorian(d2),
337-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
338-
(
339-
Self::Hebrew(c1),
340-
Self::Hebrew(c2),
341-
AnyDateInner::Hebrew(d1),
342-
AnyDateInner::Hebrew(d2),
343-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
344-
(
345-
Self::Indian(c1),
346-
Self::Indian(c2),
347-
AnyDateInner::Indian(d1),
348-
AnyDateInner::Indian(d2),
349-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
358+
options: DateDifferenceOptions,
359+
) -> Result<types::DateDuration, Self::DifferenceError> {
360+
let Ok(r) = match (self, date1, date2) {
361+
(Self::Buddhist(c1), AnyDateInner::Buddhist(d1), AnyDateInner::Buddhist(d2)) => {
362+
c1.until(d1, d2, options)
363+
}
364+
(Self::Chinese(c1), AnyDateInner::Chinese(d1), AnyDateInner::Chinese(d2)) => {
365+
c1.until(d1, d2, options)
366+
}
367+
(Self::Coptic(c1), AnyDateInner::Coptic(d1), AnyDateInner::Coptic(d2)) => {
368+
c1.until(d1, d2, options)
369+
}
370+
(Self::Dangi(c1), AnyDateInner::Dangi(d1), AnyDateInner::Dangi(d2)) => {
371+
c1.until(d1, d2, options)
372+
}
373+
(Self::Ethiopian(c1), AnyDateInner::Ethiopian(d1), AnyDateInner::Ethiopian(d2)) => {
374+
c1.until(d1, d2, options)
375+
}
376+
(Self::Gregorian(c1), AnyDateInner::Gregorian(d1), AnyDateInner::Gregorian(d2)) => {
377+
c1.until(d1, d2, options)
378+
}
379+
(Self::Hebrew(c1), AnyDateInner::Hebrew(d1), AnyDateInner::Hebrew(d2)) => {
380+
c1.until(d1, d2, options)
381+
}
382+
(Self::Indian(c1), AnyDateInner::Indian(d1), AnyDateInner::Indian(d2)) => {
383+
c1.until(d1, d2, options)
384+
}
350385
(
351386
Self::HijriTabular(c1),
352-
Self::HijriTabular(c2),
353387
&AnyDateInner::HijriTabular(ref d1, s1),
354388
&AnyDateInner::HijriTabular(ref d2, s2),
355-
) if c1.0 == c2.0 && c2.0 == s1 && s1 == s2 => {
356-
c1.until(d1, d2, c2, largest_unit, smallest_unit)
357-
}
389+
) if c1.0 == s1 && s1 == s2 => c1.until(d1, d2, options),
358390
(
359391
Self::HijriSimulated(c1),
360-
Self::HijriSimulated(c2),
361392
AnyDateInner::HijriSimulated(d1),
362393
AnyDateInner::HijriSimulated(d2),
363-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
394+
) => c1.until(d1, d2, options),
364395
(
365396
Self::HijriUmmAlQura(c1),
366-
Self::HijriUmmAlQura(c2),
367397
AnyDateInner::HijriUmmAlQura(d1),
368398
AnyDateInner::HijriUmmAlQura(d2),
369-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
370-
(Self::Iso(c1), Self::Iso(c2), AnyDateInner::Iso(d1), AnyDateInner::Iso(d2)) => {
371-
c1.until(d1, d2, c2, largest_unit, smallest_unit)
399+
) => c1.until(d1, d2, options),
400+
(Self::Iso(c1), AnyDateInner::Iso(d1), AnyDateInner::Iso(d2)) => {
401+
c1.until(d1, d2, options)
402+
}
403+
(Self::Japanese(c1), AnyDateInner::Japanese(d1), AnyDateInner::Japanese(d2)) => {
404+
c1.until(d1, d2, options)
372405
}
373-
(
374-
Self::Japanese(c1),
375-
Self::Japanese(c2),
376-
AnyDateInner::Japanese(d1),
377-
AnyDateInner::Japanese(d2),
378-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
379406
(
380407
Self::JapaneseExtended(c1),
381-
Self::JapaneseExtended(c2),
382408
AnyDateInner::JapaneseExtended(d1),
383409
AnyDateInner::JapaneseExtended(d2),
384-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
385-
(
386-
Self::Persian(c1),
387-
Self::Persian(c2),
388-
AnyDateInner::Persian(d1),
389-
AnyDateInner::Persian(d2),
390-
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
391-
(Self::Roc(c1), Self::Roc(c2), AnyDateInner::Roc(d1), AnyDateInner::Roc(d2)) => {
392-
c1.until(d1, d2, c2, largest_unit, smallest_unit)
410+
) => c1.until(d1, d2, options),
411+
(Self::Persian(c1), AnyDateInner::Persian(d1), AnyDateInner::Persian(d2)) => {
412+
c1.until(d1, d2, options)
413+
}
414+
(Self::Roc(c1), AnyDateInner::Roc(d1), AnyDateInner::Roc(d2)) => {
415+
c1.until(d1, d2, options)
393416
}
394417
_ => {
395-
// attempt to convert
396-
let iso = calendar2.to_iso(date2);
397-
398-
match_cal_and_date!(match (self, date1):
399-
(c1, d1) => {
400-
let d2 = c1.from_iso(iso);
401-
c1.until(d1, &d2, c1, largest_unit, smallest_unit)
402-
}
403-
)
418+
return Err(AnyCalendarDifferenceError::MismatchedCalendars);
404419
}
405-
}
420+
};
421+
Ok(r)
406422
}
407423

408424
fn year_info(&self, date: &Self::DateInner) -> types::YearInfo {

0 commit comments

Comments
 (0)