Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1d3648e
DateAdd, DateUntil implemented. Stuck in trait land
sffc Oct 1, 2025
2a38bfe
only failing file is abstract_gregorian
sffc Oct 1, 2025
40f70a0
fix abstract_gregorian
robertbastian Oct 1, 2025
d65db3f
Tests compiling; lots of docs tests
sffc Oct 3, 2025
7447707
More tests passing
sffc Oct 3, 2025
547ccac
All tests passing in icu_calendar?
sffc Oct 3, 2025
457b3f1
Merge branch 'main' into dateadduntil
sffc Oct 3, 2025
f683574
Delete commented-out test
sffc Oct 3, 2025
faf38ac
diplomat coverage
sffc Oct 3, 2025
d9ab920
clippy
sffc Oct 3, 2025
860c630
Add another test
sffc Oct 3, 2025
b31cf42
Actually add the file
sffc Oct 3, 2025
b332e05
Hebrew tests
sffc Oct 3, 2025
f8ec962
fmt
sffc Oct 3, 2025
c4059a4
Add more Hebrew tests
sffc Oct 3, 2025
3c43228
clippy
sffc Oct 3, 2025
f1e4dbd
Add PartialEq
Manishearth Oct 6, 2025
300361d
Add tricky Hebrew standalone test case
sffc Oct 7, 2025
a5890fd
Fix leap month arithmetic and re-enable tests
sffc Oct 7, 2025
daa6f27
Renames, docs, comments, fmt
sffc Oct 7, 2025
4b03c6d
ci
sffc Oct 7, 2025
bca8cae
Fix benchmark and set AnyCalendarDifferenceError to hidden
sffc Oct 7, 2025
ad1c54a
feedback
sffc Oct 8, 2025
f999bc7
Add test comparing against RataDie diff
sffc Oct 8, 2025
3ef45f3
Rewrite the macro to simplify code and reduce potential for bugs
sffc Oct 9, 2025
cc339fc
Merge branch 'main' into dateadduntil
sffc Oct 9, 2025
716b194
fmt, clippy
sffc Oct 9, 2025
8262b4a
Add missing docs
sffc Oct 9, 2025
ed48da7
revert controversial any_calendar refactorings
robertbastian Oct 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components/calendar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,9 @@ harness = false
name = "convert"
harness = false

[[test]]
name = "arithmetic"
required-features = ["ixdtf"]

[package.metadata.cargo-semver-checks.lints]
workspace = true
25 changes: 17 additions & 8 deletions components/calendar/benches/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,27 @@ pub struct Test {
use criterion::{
black_box, criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
};
use icu_calendar::{AsCalendar, Calendar, Date, DateDuration};
use icu_calendar::{
options::{DateAddOptions, Overflow},
types, AsCalendar, Calendar, Date,
};

fn bench_date<A: AsCalendar>(date: &mut Date<A>) {
// black_box used to avoid compiler optimization.
// Arithmetic
date.add(DateDuration {
is_negative: false,
years: black_box(1),
months: black_box(2),
weeks: black_box(3),
days: black_box(4),
});
let mut options = DateAddOptions::default();
options.overflow = Some(Overflow::Constrain);
date.try_add_with_options(
types::DateDuration {
is_negative: false,
years: black_box(1),
months: black_box(2),
weeks: black_box(3),
days: black_box(4),
},
options,
)
.unwrap();

// Retrieving vals
let _ = black_box(date.year());
Expand Down
236 changes: 126 additions & 110 deletions components/calendar/src/any_calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ use crate::cal::iso::IsoDateInner;
use crate::cal::*;
use crate::error::DateError;
use crate::options::DateFromFieldsOptions;
use crate::options::{DateAddOptions, DateDifferenceOptions};
use crate::types::{DateFields, YearInfo};
use crate::{types, AsCalendar, Calendar, Date, DateDuration, DateDurationUnit, Ref};
use crate::{types, AsCalendar, Calendar, Date, Ref};

use crate::preferences::{CalendarAlgorithm, HijriCalendarAlgorithm};
use icu_locale_core::preferences::define_preferences;
Expand Down Expand Up @@ -211,10 +212,45 @@ macro_rules! match_cal {
};
}

/// Error returned when comparing two [`Date`]s with [`AnyCalendar`].
#[derive(Clone, Copy, PartialEq, Debug)]
#[non_exhaustive]
#[doc(hidden)] // unstable, not yet graduated
pub enum AnyCalendarDifferenceError {
/// The calendars of the two dates being compared are not equal.
///
/// To compare dates in different calendars, convert them to the same calendar first.
///
/// # Examples
///
/// ```
/// use icu::calendar::Date;
/// use icu::calendar::cal::AnyCalendarDifferenceError;
///
/// let d1 = Date::try_new_gregorian(2000, 1, 1).unwrap().to_any();
/// let d2 = Date::try_new_hebrew(5780, 1, 1).unwrap().to_any();
///
/// assert!(matches!(
/// d1.try_until_with_options(&d2, Default::default()),
/// Err(AnyCalendarDifferenceError::MismatchedCalendars),
/// ));
///
/// // To compare the dates, convert them to the same calendar,
/// // such as ISO.
///
/// assert!(matches!(
/// d1.to_iso().try_until_with_options(&d2.to_iso(), Default::default()),
/// Ok(_)
/// ));
/// ```
MismatchedCalendars,
}

impl crate::cal::scaffold::UnstableSealed for AnyCalendar {}
impl Calendar for AnyCalendar {
type DateInner = AnyDateInner;
type Year = YearInfo;
type DifferenceError = AnyCalendarDifferenceError;

fn from_fields(
&self,
Expand Down Expand Up @@ -252,34 +288,58 @@ impl Calendar for AnyCalendar {
match_cal_and_date!(match (self, date): (c, d) => c.days_in_month(d))
}

fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) {
match (self, date) {
(Self::Buddhist(c), AnyDateInner::Buddhist(ref mut d)) => c.offset_date(d, offset),
(Self::Chinese(c), AnyDateInner::Chinese(ref mut d)) => c.offset_date(d, offset),
(Self::Coptic(c), AnyDateInner::Coptic(ref mut d)) => c.offset_date(d, offset),
(Self::Dangi(c), AnyDateInner::Dangi(ref mut d)) => c.offset_date(d, offset),
(Self::Ethiopian(c), AnyDateInner::Ethiopian(ref mut d)) => c.offset_date(d, offset),
(Self::Gregorian(c), AnyDateInner::Gregorian(ref mut d)) => c.offset_date(d, offset),
(Self::Hebrew(c), AnyDateInner::Hebrew(ref mut d)) => c.offset_date(d, offset),
(Self::Indian(c), AnyDateInner::Indian(ref mut d)) => c.offset_date(d, offset),
(Self::HijriTabular(c), &mut AnyDateInner::HijriTabular(ref mut d, sighting))
if c.0 == sighting =>
fn add(
&self,
date: &Self::DateInner,
duration: types::DateDuration,
options: DateAddOptions,
) -> Result<Self::DateInner, DateError> {
let mut date = *date;
match (self, &mut date) {
(Self::Buddhist(c), AnyDateInner::Buddhist(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Chinese(c), AnyDateInner::Chinese(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Coptic(c), AnyDateInner::Coptic(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Dangi(c), AnyDateInner::Dangi(ref mut d)) => *d = c.add(d, duration, options)?,
(Self::Ethiopian(c), AnyDateInner::Ethiopian(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Gregorian(c), AnyDateInner::Gregorian(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Hebrew(c), AnyDateInner::Hebrew(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Indian(c), AnyDateInner::Indian(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::HijriTabular(c), AnyDateInner::HijriTabular(ref mut d, sighting))
if c.0 == *sighting =>
{
c.offset_date(d, offset)
*d = c.add(d, duration, options)?
}
(Self::HijriSimulated(c), AnyDateInner::HijriSimulated(ref mut d)) => {
c.offset_date(d, offset)
*d = c.add(d, duration, options)?
}
(Self::HijriUmmAlQura(c), AnyDateInner::HijriUmmAlQura(ref mut d)) => {
c.offset_date(d, offset)
*d = c.add(d, duration, options)?
}
(Self::Iso(c), AnyDateInner::Iso(ref mut d)) => *d = c.add(d, duration, options)?,
(Self::Japanese(c), AnyDateInner::Japanese(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Iso(c), AnyDateInner::Iso(ref mut d)) => c.offset_date(d, offset),
(Self::Japanese(c), AnyDateInner::Japanese(ref mut d)) => c.offset_date(d, offset),
(Self::JapaneseExtended(c), AnyDateInner::JapaneseExtended(ref mut d)) => {
c.offset_date(d, offset)
*d = c.add(d, duration, options)?
}
(Self::Persian(c), AnyDateInner::Persian(ref mut d)) => c.offset_date(d, offset),
(Self::Roc(c), AnyDateInner::Roc(ref mut d)) => c.offset_date(d, offset),
(Self::Persian(c), AnyDateInner::Persian(ref mut d)) => {
*d = c.add(d, duration, options)?
}
(Self::Roc(c), AnyDateInner::Roc(ref mut d)) => *d = c.add(d, duration, options)?,
// This is only reached from misuse of from_raw, a semi-internal api
#[expect(clippy::panic)]
(_, d) => panic!(
Expand All @@ -288,121 +348,77 @@ impl Calendar for AnyCalendar {
d.kind().debug_name()
),
}
Ok(date)
}

fn until(
&self,
date1: &Self::DateInner,
date2: &Self::DateInner,
calendar2: &Self,
largest_unit: DateDurationUnit,
smallest_unit: DateDurationUnit,
) -> DateDuration {
match (self, calendar2, date1, date2) {
(
Self::Buddhist(c1),
Self::Buddhist(c2),
AnyDateInner::Buddhist(d1),
AnyDateInner::Buddhist(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Chinese(c1),
Self::Chinese(c2),
AnyDateInner::Chinese(d1),
AnyDateInner::Chinese(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Coptic(c1),
Self::Coptic(c2),
AnyDateInner::Coptic(d1),
AnyDateInner::Coptic(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Dangi(c1),
Self::Dangi(c2),
AnyDateInner::Dangi(d1),
AnyDateInner::Dangi(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Ethiopian(c1),
Self::Ethiopian(c2),
AnyDateInner::Ethiopian(d1),
AnyDateInner::Ethiopian(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Gregorian(c1),
Self::Gregorian(c2),
AnyDateInner::Gregorian(d1),
AnyDateInner::Gregorian(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Hebrew(c1),
Self::Hebrew(c2),
AnyDateInner::Hebrew(d1),
AnyDateInner::Hebrew(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Indian(c1),
Self::Indian(c2),
AnyDateInner::Indian(d1),
AnyDateInner::Indian(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
options: DateDifferenceOptions,
) -> Result<types::DateDuration, Self::DifferenceError> {
let Ok(r) = match (self, date1, date2) {
(Self::Buddhist(c1), AnyDateInner::Buddhist(d1), AnyDateInner::Buddhist(d2)) => {
c1.until(d1, d2, options)
}
(Self::Chinese(c1), AnyDateInner::Chinese(d1), AnyDateInner::Chinese(d2)) => {
c1.until(d1, d2, options)
}
(Self::Coptic(c1), AnyDateInner::Coptic(d1), AnyDateInner::Coptic(d2)) => {
c1.until(d1, d2, options)
}
(Self::Dangi(c1), AnyDateInner::Dangi(d1), AnyDateInner::Dangi(d2)) => {
c1.until(d1, d2, options)
}
(Self::Ethiopian(c1), AnyDateInner::Ethiopian(d1), AnyDateInner::Ethiopian(d2)) => {
c1.until(d1, d2, options)
}
(Self::Gregorian(c1), AnyDateInner::Gregorian(d1), AnyDateInner::Gregorian(d2)) => {
c1.until(d1, d2, options)
}
(Self::Hebrew(c1), AnyDateInner::Hebrew(d1), AnyDateInner::Hebrew(d2)) => {
c1.until(d1, d2, options)
}
(Self::Indian(c1), AnyDateInner::Indian(d1), AnyDateInner::Indian(d2)) => {
c1.until(d1, d2, options)
}
(
Self::HijriTabular(c1),
Self::HijriTabular(c2),
&AnyDateInner::HijriTabular(ref d1, s1),
&AnyDateInner::HijriTabular(ref d2, s2),
) if c1.0 == c2.0 && c2.0 == s1 && s1 == s2 => {
c1.until(d1, d2, c2, largest_unit, smallest_unit)
}
) if c1.0 == s1 && s1 == s2 => c1.until(d1, d2, options),
(
Self::HijriSimulated(c1),
Self::HijriSimulated(c2),
AnyDateInner::HijriSimulated(d1),
AnyDateInner::HijriSimulated(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
) => c1.until(d1, d2, options),
(
Self::HijriUmmAlQura(c1),
Self::HijriUmmAlQura(c2),
AnyDateInner::HijriUmmAlQura(d1),
AnyDateInner::HijriUmmAlQura(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(Self::Iso(c1), Self::Iso(c2), AnyDateInner::Iso(d1), AnyDateInner::Iso(d2)) => {
c1.until(d1, d2, c2, largest_unit, smallest_unit)
) => c1.until(d1, d2, options),
(Self::Iso(c1), AnyDateInner::Iso(d1), AnyDateInner::Iso(d2)) => {
c1.until(d1, d2, options)
}
(Self::Japanese(c1), AnyDateInner::Japanese(d1), AnyDateInner::Japanese(d2)) => {
c1.until(d1, d2, options)
}
(
Self::Japanese(c1),
Self::Japanese(c2),
AnyDateInner::Japanese(d1),
AnyDateInner::Japanese(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::JapaneseExtended(c1),
Self::JapaneseExtended(c2),
AnyDateInner::JapaneseExtended(d1),
AnyDateInner::JapaneseExtended(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(
Self::Persian(c1),
Self::Persian(c2),
AnyDateInner::Persian(d1),
AnyDateInner::Persian(d2),
) => c1.until(d1, d2, c2, largest_unit, smallest_unit),
(Self::Roc(c1), Self::Roc(c2), AnyDateInner::Roc(d1), AnyDateInner::Roc(d2)) => {
c1.until(d1, d2, c2, largest_unit, smallest_unit)
) => c1.until(d1, d2, options),
(Self::Persian(c1), AnyDateInner::Persian(d1), AnyDateInner::Persian(d2)) => {
c1.until(d1, d2, options)
}
(Self::Roc(c1), AnyDateInner::Roc(d1), AnyDateInner::Roc(d2)) => {
c1.until(d1, d2, options)
}
_ => {
// attempt to convert
let iso = calendar2.to_iso(date2);

match_cal_and_date!(match (self, date1):
(c1, d1) => {
let d2 = c1.from_iso(iso);
c1.until(d1, &d2, c1, largest_unit, smallest_unit)
}
)
return Err(AnyCalendarDifferenceError::MismatchedCalendars);
}
}
};
Ok(r)
}

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