diff --git a/components/calendar/Cargo.toml b/components/calendar/Cargo.toml index c25018a3adf..8c34fd5b68f 100644 --- a/components/calendar/Cargo.toml +++ b/components/calendar/Cargo.toml @@ -66,5 +66,9 @@ harness = false name = "convert" harness = false +[[test]] +name = "arithmetic" +required-features = ["ixdtf"] + [package.metadata.cargo-semver-checks.lints] workspace = true diff --git a/components/calendar/benches/date.rs b/components/calendar/benches/date.rs index cbb0933e337..d4c1d947b45 100644 --- a/components/calendar/benches/date.rs +++ b/components/calendar/benches/date.rs @@ -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(date: &mut Date) { // 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()); diff --git a/components/calendar/src/any_calendar.rs b/components/calendar/src/any_calendar.rs index 694a247748e..99669de2ba1 100644 --- a/components/calendar/src/any_calendar.rs +++ b/components/calendar/src/any_calendar.rs @@ -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; @@ -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, @@ -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 { + 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!( @@ -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 { + 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 { diff --git a/components/calendar/src/cal/abstract_gregorian.rs b/components/calendar/src/cal/abstract_gregorian.rs index 3f05f8eccad..164b6e2ec20 100644 --- a/components/calendar/src/cal/abstract_gregorian.rs +++ b/components/calendar/src/cal/abstract_gregorian.rs @@ -8,9 +8,10 @@ use crate::calendar_arithmetic::{ }; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::preferences::CalendarAlgorithm; use crate::types::EraYear; -use crate::{types, Calendar, DateDuration, DateDurationUnit, RangeError}; +use crate::{types, Calendar, RangeError}; use calendrical_calculations::helpers::I32CastError; use calendrical_calculations::rata_die::RataDie; @@ -43,8 +44,6 @@ impl ArithmeticDate> { } impl CalendarArithmetic for AbstractGregorian { - type YearInfo = i32; - fn days_in_provided_month(year: i32, month: u8) -> u8 { // https://www.youtube.com/watch?v=J9KijLyP-yg&t=1394s if month == 2 { @@ -113,6 +112,7 @@ impl crate::cal::scaffold::UnstableSealed for AbstractGregori impl Calendar for AbstractGregorian { type DateInner = ArithmeticDate>; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, @@ -156,19 +156,22 @@ impl Calendar for AbstractGregorian { date.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.offset_date(offset, &()); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.added(duration, &AbstractGregorian(IsoEra), options) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.until(*date2, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.until(date2, &AbstractGregorian(IsoEra), options)) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { @@ -181,7 +184,7 @@ impl Calendar for AbstractGregorian { } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.month() + self.month_code_from_ordinal(&date.year, date.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { @@ -214,6 +217,7 @@ macro_rules! impl_with_abstract_gregorian { impl crate::Calendar for $cal_ty { type DateInner = $inner_date_ty; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, fields: crate::types::DateFields, @@ -265,30 +269,27 @@ macro_rules! impl_with_abstract_gregorian { crate::cal::abstract_gregorian::AbstractGregorian($eras_expr).days_in_month(&date.0) } - fn offset_date(&self, date: &mut Self::DateInner, offset: crate::DateDuration) { + fn add( + &self, + date: &Self::DateInner, + duration: crate::types::DateDuration, + options: crate::options::DateAddOptions, + ) -> Result { let $self_ident = self; - let mut inner = date.0; crate::cal::abstract_gregorian::AbstractGregorian($eras_expr) - .offset_date(&mut inner, offset); - date.0 = inner; + .add(&date.0, duration, options) + .map($inner_date_ty) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - largest_unit: crate::DateDurationUnit, - smallest_unit: crate::DateDurationUnit, - ) -> crate::DateDuration { + options: crate::options::DateDifferenceOptions, + ) -> Result { let $self_ident = self; - crate::cal::abstract_gregorian::AbstractGregorian($eras_expr).until( - &date1.0, - &date2.0, - &crate::cal::abstract_gregorian::AbstractGregorian($eras_expr), - largest_unit, - smallest_unit, - ) + crate::cal::abstract_gregorian::AbstractGregorian($eras_expr) + .until(&date1.0, &date2.0, options) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { diff --git a/components/calendar/src/cal/chinese.rs b/components/calendar/src/cal/chinese.rs index 93cb0c92cdd..02de629a103 100644 --- a/components/calendar/src/cal/chinese.rs +++ b/components/calendar/src/cal/chinese.rs @@ -3,14 +3,17 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use crate::cal::iso::{Iso, IsoDateInner}; -use crate::calendar_arithmetic::{ArithmeticDate, ArithmeticDateBuilder, CalendarArithmetic}; +use crate::calendar_arithmetic::{ + ArithmeticDate, ArithmeticDateBuilder, CalendarArithmetic, ToExtendedYear, +}; use crate::calendar_arithmetic::{DateFieldsResolver, PrecomputedDataSource}; use crate::error::DateError; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, Overflow}; use crate::provider::chinese_based::PackedChineseBasedYearInfo; use crate::types::{MonthCode, MonthInfo}; use crate::AsCalendar; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit}; +use crate::{types, Calendar, Date}; use calendrical_calculations::chinese_based::{ self, ChineseBased, YearBounds, WELL_BEHAVED_ASTRONOMICAL_RANGE, }; @@ -497,8 +500,6 @@ impl LunarChinese { } impl CalendarArithmetic for LunarChinese { - type YearInfo = LunarChineseYearData; - fn days_in_provided_month(year: LunarChineseYearData, month: u8) -> u8 { year.days_in_month(month) } @@ -577,12 +578,21 @@ impl DateFieldsResolver for LunarChinese { _ => Err(DateError::UnknownMonthCode(month_code)), } } + + fn month_code_from_ordinal( + &self, + year: &Self::YearInfo, + ordinal_month: u8, + ) -> types::MonthInfo { + year.month(ordinal_month) + } } impl crate::cal::scaffold::UnstableSealed for LunarChinese {} impl Calendar for LunarChinese { type DateInner = ChineseDateInner; type Year = types::CyclicYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, @@ -645,25 +655,22 @@ impl Calendar for LunarChinese { date.0.days_in_month() } - #[doc(hidden)] // unstable - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, self); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(ChineseDateInner) } - #[doc(hidden)] // unstable - /// Calculate `date2 - date` as a duration - /// - /// `calendar2` is the calendar object associated with `date2`. In case the specific calendar objects - /// differ on date, the date for the first calendar is used, and `date2` may be converted if necessary. fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } /// Obtain a name for the calendar for debug printing @@ -760,9 +767,9 @@ pub struct LunarChineseYearData { pub(crate) related_iso: i32, } -impl From for i32 { - fn from(value: LunarChineseYearData) -> Self { - value.related_iso +impl ToExtendedYear for LunarChineseYearData { + fn to_extended_year(&self) -> i32 { + self.related_iso } } diff --git a/components/calendar/src/cal/coptic.rs b/components/calendar/src/cal/coptic.rs index ed16e04c7fd..bd6b1dcf04c 100644 --- a/components/calendar/src/cal/coptic.rs +++ b/components/calendar/src/cal/coptic.rs @@ -7,7 +7,8 @@ use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, DateFieldsResolver}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError}; +use crate::options::{DateAddOptions, DateDifferenceOptions}; +use crate::{types, Calendar, Date, RangeError}; use calendrical_calculations::helpers::I32CastError; use calendrical_calculations::rata_die::RataDie; use tinystr::tinystr; @@ -39,8 +40,6 @@ pub struct Coptic; pub struct CopticDateInner(pub(crate) ArithmeticDate); impl CalendarArithmetic for Coptic { - type YearInfo = i32; - fn days_in_provided_month(year: i32, month: u8) -> u8 { if (1..=12).contains(&month) { 30 @@ -133,6 +132,8 @@ impl crate::cal::scaffold::UnstableSealed for Coptic {} impl Calendar for Coptic { type DateInner = CopticDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; + fn from_fields( &self, fields: types::DateFields, @@ -178,19 +179,22 @@ impl Calendar for Coptic { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, &()); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(CopticDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { @@ -209,7 +213,7 @@ impl Calendar for Coptic { } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.0.month() + self.month_code_from_ordinal(&date.0.year, date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { diff --git a/components/calendar/src/cal/ethiopian.rs b/components/calendar/src/cal/ethiopian.rs index 77fd72e4838..9e53ee59954 100644 --- a/components/calendar/src/cal/ethiopian.rs +++ b/components/calendar/src/cal/ethiopian.rs @@ -8,8 +8,9 @@ use crate::cal::Coptic; use crate::calendar_arithmetic::{ArithmeticDate, ArithmeticDateBuilder, DateFieldsResolver}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::DateFields; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError}; +use crate::{types, Calendar, Date, RangeError}; use calendrical_calculations::rata_die::RataDie; use tinystr::tinystr; @@ -103,6 +104,8 @@ impl crate::cal::scaffold::UnstableSealed for Ethiopian {} impl Calendar for Ethiopian { type DateInner = EthiopianDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; + fn from_fields( &self, fields: DateFields, @@ -143,19 +146,24 @@ impl Calendar for Ethiopian { Coptic.days_in_month(&date.0) } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - Coptic.offset_date(&mut date.0, offset); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + Coptic + .add(&date.0, duration, options) + .map(EthiopianDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - largest_unit: DateDurationUnit, - smallest_unit: DateDurationUnit, - ) -> DateDuration { - Coptic.until(&date1.0, &date2.0, &Coptic, largest_unit, smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Coptic.until(&date1.0, &date2.0, options) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { diff --git a/components/calendar/src/cal/hebrew.rs b/components/calendar/src/cal/hebrew.rs index 7ea115b8ace..1767ea048bc 100644 --- a/components/calendar/src/cal/hebrew.rs +++ b/components/calendar/src/cal/hebrew.rs @@ -3,13 +3,16 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use crate::cal::iso::{Iso, IsoDateInner}; -use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic, DateFieldsResolver}; +use crate::calendar_arithmetic::{ + ArithmeticDate, CalendarArithmetic, DateFieldsResolver, ToExtendedYear, +}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, PrecomputedDataSource}; use crate::error::DateError; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, Overflow}; use crate::types::{DateFields, MonthInfo}; use crate::RangeError; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit}; +use crate::{types, Calendar, Date}; use ::tinystr::tinystr; use calendrical_calculations::hebrew_keviyah::{Keviyah, YearInfo}; use calendrical_calculations::rata_die::RataDie; @@ -55,9 +58,9 @@ pub(crate) struct HebrewYearInfo { value: i32, } -impl From for i32 { - fn from(value: HebrewYearInfo) -> Self { - value.value +impl ToExtendedYear for HebrewYearInfo { + fn to_extended_year(&self) -> i32 { + self.value } } @@ -85,8 +88,6 @@ impl HebrewYearInfo { // HEBREW CALENDAR impl CalendarArithmetic for Hebrew { - type YearInfo = HebrewYearInfo; - fn days_in_provided_month(info: HebrewYearInfo, ordinal_month: u8) -> u8 { info.keviyah.month_len(ordinal_month) } @@ -226,12 +227,65 @@ impl DateFieldsResolver for Hebrew { }; Ok(ordinal_month) } + + fn month_code_from_ordinal( + &self, + year: &Self::YearInfo, + ordinal_month: u8, + ) -> types::MonthInfo { + let mut ordinal = ordinal_month; + let is_leap_year = Self::provided_year_is_leap(*year); + + if is_leap_year { + if ordinal == 6 { + return types::MonthInfo { + ordinal, + standard_code: types::MonthCode(tinystr!(4, "M05L")), + formatting_code: types::MonthCode(tinystr!(4, "M05L")), + }; + } else if ordinal == 7 { + return types::MonthInfo { + ordinal, + // Adar II is the same as Adar and has the same code + standard_code: types::MonthCode(tinystr!(4, "M06")), + formatting_code: types::MonthCode(tinystr!(4, "M06L")), + }; + } + } + + if is_leap_year && ordinal > 6 { + ordinal -= 1; + } + + let code = match ordinal { + 1 => tinystr!(4, "M01"), + 2 => tinystr!(4, "M02"), + 3 => tinystr!(4, "M03"), + 4 => tinystr!(4, "M04"), + 5 => tinystr!(4, "M05"), + 6 => tinystr!(4, "M06"), + 7 => tinystr!(4, "M07"), + 8 => tinystr!(4, "M08"), + 9 => tinystr!(4, "M09"), + 10 => tinystr!(4, "M10"), + 11 => tinystr!(4, "M11"), + 12 => tinystr!(4, "M12"), + _ => tinystr!(4, "und"), + }; + + types::MonthInfo { + ordinal: ordinal_month, + standard_code: types::MonthCode(code), + formatting_code: types::MonthCode(code), + } + } } impl crate::cal::scaffold::UnstableSealed for Hebrew {} impl Calendar for Hebrew { type DateInner = HebrewDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, @@ -285,19 +339,22 @@ impl Calendar for Hebrew { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, &()) + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(HebrewDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } fn debug_name(&self) -> &'static str { @@ -320,51 +377,7 @@ impl Calendar for Hebrew { } fn month(&self, date: &Self::DateInner) -> MonthInfo { - let mut ordinal = date.0.month; - let is_leap_year = Self::provided_year_is_leap(date.0.year); - - if is_leap_year { - if ordinal == 6 { - return types::MonthInfo { - ordinal, - standard_code: types::MonthCode(tinystr!(4, "M05L")), - formatting_code: types::MonthCode(tinystr!(4, "M05L")), - }; - } else if ordinal == 7 { - return types::MonthInfo { - ordinal, - // Adar II is the same as Adar and has the same code - standard_code: types::MonthCode(tinystr!(4, "M06")), - formatting_code: types::MonthCode(tinystr!(4, "M06L")), - }; - } - } - - if is_leap_year && ordinal > 6 { - ordinal -= 1; - } - - let code = match ordinal { - 1 => tinystr!(4, "M01"), - 2 => tinystr!(4, "M02"), - 3 => tinystr!(4, "M03"), - 4 => tinystr!(4, "M04"), - 5 => tinystr!(4, "M05"), - 6 => tinystr!(4, "M06"), - 7 => tinystr!(4, "M07"), - 8 => tinystr!(4, "M08"), - 9 => tinystr!(4, "M09"), - 10 => tinystr!(4, "M10"), - 11 => tinystr!(4, "M11"), - 12 => tinystr!(4, "M12"), - _ => tinystr!(4, "und"), - }; - - types::MonthInfo { - ordinal: date.0.month, - standard_code: types::MonthCode(code), - formatting_code: types::MonthCode(code), - } + self.month_code_from_ordinal(&date.0.year, date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { diff --git a/components/calendar/src/cal/hijri.rs b/components/calendar/src/cal/hijri.rs index 5507b3b6698..c48510cf913 100644 --- a/components/calendar/src/cal/hijri.rs +++ b/components/calendar/src/cal/hijri.rs @@ -3,14 +3,15 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use crate::cal::iso::{Iso, IsoDateInner}; -use crate::calendar_arithmetic::PrecomputedDataSource; use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, DateFieldsResolver}; +use crate::calendar_arithmetic::{PrecomputedDataSource, ToExtendedYear}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::provider::hijri::PackedHijriYearInfo; use crate::types::DateFields; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit}; +use crate::{types, Calendar, Date}; use crate::{AsCalendar, RangeError}; use calendrical_calculations::islamic::{ ISLAMIC_EPOCH_FRIDAY, ISLAMIC_EPOCH_THURSDAY, WELL_BEHAVED_ASTRONOMICAL_RANGE, @@ -485,9 +486,9 @@ pub struct HijriYearData { extended_year: i32, } -impl From for i32 { - fn from(value: HijriYearData) -> Self { - value.extended_year +impl ToExtendedYear for HijriYearData { + fn to_extended_year(&self) -> i32 { + self.extended_year } } @@ -657,8 +658,6 @@ impl PartialEq for HijriDateInner { impl Eq for HijriDateInner {} impl CalendarArithmetic for Hijri { - type YearInfo = HijriYearData; - fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8 { if year.packed.month_has_30_days(month) { 30 @@ -720,6 +719,8 @@ impl crate::cal::scaffold::UnstableSealed for Hijri {} impl Calendar for Hijri { type DateInner = HijriDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; + fn from_fields( &self, fields: DateFields, @@ -780,19 +781,22 @@ impl Calendar for Hijri { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, self) + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(HijriDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } fn debug_name(&self) -> &'static str { @@ -825,7 +829,7 @@ impl Calendar for Hijri { } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.0.month() + self.month_code_from_ordinal(&date.0.year, date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { diff --git a/components/calendar/src/cal/indian.rs b/components/calendar/src/cal/indian.rs index 291b0424274..3ff9912eb41 100644 --- a/components/calendar/src/cal/indian.rs +++ b/components/calendar/src/cal/indian.rs @@ -7,8 +7,9 @@ use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, DateFieldsResolver}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::DateFields; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError}; +use crate::{types, Calendar, Date, RangeError}; use calendrical_calculations::rata_die::RataDie; use tinystr::tinystr; @@ -34,8 +35,6 @@ pub struct Indian; pub struct IndianDateInner(ArithmeticDate); impl CalendarArithmetic for Indian { - type YearInfo = i32; - fn days_in_provided_month(year: i32, month: u8) -> u8 { if month == 1 { 30 + Self::provided_year_is_leap(year) as u8 @@ -115,6 +114,8 @@ impl crate::cal::scaffold::UnstableSealed for Indian {} impl Calendar for Indian { type DateInner = IndianDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; + fn from_fields( &self, fields: DateFields, @@ -185,19 +186,22 @@ impl Calendar for Indian { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, &()); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(IndianDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { @@ -216,7 +220,7 @@ impl Calendar for Indian { } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.0.month() + self.month_code_from_ordinal(&date.0.year, date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { diff --git a/components/calendar/src/cal/iso.rs b/components/calendar/src/cal/iso.rs index 034b1188778..b701902f83a 100644 --- a/components/calendar/src/cal/iso.rs +++ b/components/calendar/src/cal/iso.rs @@ -81,9 +81,8 @@ impl Iso { #[cfg(test)] mod test { use super::*; - use crate::types::{RataDie, Weekday}; + use crate::types::{DateDuration, RataDie, Weekday}; use crate::Calendar; - use crate::DateDuration; #[test] fn iso_overflow() { @@ -274,12 +273,16 @@ mod test { fn test_offset() { let today = Date::try_new_iso(2021, 6, 23).unwrap(); let today_plus_5000 = Date::try_new_iso(2035, 3, 2).unwrap(); - let offset = today.added(DateDuration::for_days(5000)); + let offset = today + .try_added_with_options(DateDuration::for_days(5000), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_5000); let today = Date::try_new_iso(2021, 6, 23).unwrap(); let today_minus_5000 = Date::try_new_iso(2007, 10, 15).unwrap(); - let offset = today.added(DateDuration::for_days(-5000)); + let offset = today + .try_added_with_options(DateDuration::for_days(-5000), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_5000); } @@ -287,32 +290,44 @@ mod test { fn test_offset_at_month_boundary() { let today = Date::try_new_iso(2020, 2, 28).unwrap(); let today_plus_2 = Date::try_new_iso(2020, 3, 1).unwrap(); - let offset = today.added(DateDuration::for_days(2)); + let offset = today + .try_added_with_options(DateDuration::for_days(2), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_2); let today = Date::try_new_iso(2020, 2, 28).unwrap(); let today_plus_3 = Date::try_new_iso(2020, 3, 2).unwrap(); - let offset = today.added(DateDuration::for_days(3)); + let offset = today + .try_added_with_options(DateDuration::for_days(3), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_3); let today = Date::try_new_iso(2020, 2, 28).unwrap(); let today_plus_1 = Date::try_new_iso(2020, 2, 29).unwrap(); - let offset = today.added(DateDuration::for_days(1)); + let offset = today + .try_added_with_options(DateDuration::for_days(1), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_1); let today = Date::try_new_iso(2019, 2, 28).unwrap(); let today_plus_2 = Date::try_new_iso(2019, 3, 2).unwrap(); - let offset = today.added(DateDuration::for_days(2)); + let offset = today + .try_added_with_options(DateDuration::for_days(2), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_2); let today = Date::try_new_iso(2019, 2, 28).unwrap(); let today_plus_1 = Date::try_new_iso(2019, 3, 1).unwrap(); - let offset = today.added(DateDuration::for_days(1)); + let offset = today + .try_added_with_options(DateDuration::for_days(1), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_1); let today = Date::try_new_iso(2020, 3, 1).unwrap(); let today_minus_1 = Date::try_new_iso(2020, 2, 29).unwrap(); - let offset = today.added(DateDuration::for_days(-1)); + let offset = today + .try_added_with_options(DateDuration::for_days(-1), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_1); } @@ -320,41 +335,57 @@ mod test { fn test_offset_handles_negative_month_offset() { let today = Date::try_new_iso(2020, 3, 1).unwrap(); let today_minus_2_months = Date::try_new_iso(2020, 1, 1).unwrap(); - let offset = today.added(DateDuration::for_months(-2)); + let offset = today + .try_added_with_options(DateDuration::for_months(-2), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_2_months); let today = Date::try_new_iso(2020, 3, 1).unwrap(); let today_minus_4_months = Date::try_new_iso(2019, 11, 1).unwrap(); - let offset = today.added(DateDuration::for_months(-4)); + let offset = today + .try_added_with_options(DateDuration::for_months(-4), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_4_months); let today = Date::try_new_iso(2020, 3, 1).unwrap(); let today_minus_24_months = Date::try_new_iso(2018, 3, 1).unwrap(); - let offset = today.added(DateDuration::for_months(-24)); + let offset = today + .try_added_with_options(DateDuration::for_months(-24), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_24_months); let today = Date::try_new_iso(2020, 3, 1).unwrap(); let today_minus_27_months = Date::try_new_iso(2017, 12, 1).unwrap(); - let offset = today.added(DateDuration::for_months(-27)); + let offset = today + .try_added_with_options(DateDuration::for_months(-27), Default::default()) + .unwrap(); assert_eq!(offset, today_minus_27_months); } #[test] fn test_offset_handles_out_of_bound_month_offset() { let today = Date::try_new_iso(2021, 1, 31).unwrap(); - // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28 - let today_plus_1_month = Date::try_new_iso(2021, 3, 3).unwrap(); - let offset = today.added(DateDuration::for_months(1)); + // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by constraining to the last day in February + let today_plus_1_month = Date::try_new_iso(2021, 2, 28).unwrap(); + let offset = today + .try_added_with_options(DateDuration::for_months(1), Default::default()) + .unwrap(); assert_eq!(offset, today_plus_1_month); let today = Date::try_new_iso(2021, 1, 31).unwrap(); - // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by adding 3 days to 2021/02/28 - let today_plus_1_month_1_day = Date::try_new_iso(2021, 3, 4).unwrap(); - let offset = today.added(DateDuration { - months: 1, - days: 1, - ..Default::default() - }); + // since 2021/02/31 isn't a valid date, `offset_date` auto-adjusts by constraining to the last day in February + // and then adding the days + let today_plus_1_month_1_day = Date::try_new_iso(2021, 3, 1).unwrap(); + let offset = today + .try_added_with_options( + DateDuration { + months: 1, + days: 1, + ..Default::default() + }, + Default::default(), + ) + .unwrap(); assert_eq!(offset, today_plus_1_month_1_day); } diff --git a/components/calendar/src/cal/julian.rs b/components/calendar/src/cal/julian.rs index d17d78e059e..db8d372a125 100644 --- a/components/calendar/src/cal/julian.rs +++ b/components/calendar/src/cal/julian.rs @@ -7,8 +7,9 @@ use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, DateFieldsResolver}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::DateFields; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError}; +use crate::{types, Calendar, Date, RangeError}; use calendrical_calculations::helpers::I32CastError; use calendrical_calculations::rata_die::RataDie; use tinystr::tinystr; @@ -38,8 +39,6 @@ pub struct Julian; pub struct JulianDateInner(pub(crate) ArithmeticDate); impl CalendarArithmetic for Julian { - type YearInfo = i32; - fn days_in_provided_month(year: i32, month: u8) -> u8 { match month { 4 | 6 | 9 | 11 => 30, @@ -112,6 +111,7 @@ impl crate::cal::scaffold::UnstableSealed for Julian {} impl Calendar for Julian { type DateInner = JulianDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, @@ -158,19 +158,22 @@ impl Calendar for Julian { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, &()); + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(JulianDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } /// The calendar-specific year represented by `date` @@ -202,7 +205,7 @@ impl Calendar for Julian { /// The calendar-specific month represented by `date` fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.0.month() + self.month_code_from_ordinal(&date.0.year, date.0.month) } /// The calendar-specific day-of-month represented by `date` diff --git a/components/calendar/src/cal/mod.rs b/components/calendar/src/cal/mod.rs index 6d998518a75..1b18843b467 100644 --- a/components/calendar/src/cal/mod.rs +++ b/components/calendar/src/cal/mod.rs @@ -67,7 +67,7 @@ pub type Dangi = LunarChinese; #[deprecated] pub type Chinese = LunarChinese; -pub use crate::any_calendar::{AnyCalendar, AnyCalendarKind}; +pub use crate::any_calendar::{AnyCalendar, AnyCalendarDifferenceError, AnyCalendarKind}; /// Internal scaffolding types pub mod scaffold { diff --git a/components/calendar/src/cal/persian.rs b/components/calendar/src/cal/persian.rs index b2bd08d0dde..c67aa9d566a 100644 --- a/components/calendar/src/cal/persian.rs +++ b/components/calendar/src/cal/persian.rs @@ -7,8 +7,9 @@ use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic}; use crate::calendar_arithmetic::{ArithmeticDateBuilder, DateFieldsResolver}; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::DateFields; -use crate::{types, Calendar, Date, DateDuration, DateDurationUnit, RangeError}; +use crate::{types, Calendar, Date, RangeError}; use ::tinystr::tinystr; use calendrical_calculations::helpers::I32CastError; use calendrical_calculations::rata_die::RataDie; @@ -37,8 +38,6 @@ pub struct Persian; pub struct PersianDateInner(ArithmeticDate); impl CalendarArithmetic for Persian { - type YearInfo = i32; - fn days_in_provided_month(year: i32, month: u8) -> u8 { match month { 1..=6 => 31, @@ -114,6 +113,7 @@ impl crate::cal::scaffold::UnstableSealed for Persian {} impl Calendar for Persian { type DateInner = PersianDateInner; type Year = types::EraYear; + type DifferenceError = core::convert::Infallible; fn from_fields( &self, @@ -164,19 +164,22 @@ impl Calendar for Persian { date.0.days_in_month() } - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration) { - date.0.offset_date(offset, &()) + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + date.0.added(duration, self, options).map(PersianDateInner) } fn until( &self, date1: &Self::DateInner, date2: &Self::DateInner, - _calendar2: &Self, - _largest_unit: DateDurationUnit, - _smallest_unit: DateDurationUnit, - ) -> DateDuration { - date1.0.until(date2.0, _largest_unit, _smallest_unit) + options: DateDifferenceOptions, + ) -> Result { + Ok(date1.0.until(&date2.0, self, options)) } fn year_info(&self, date: &Self::DateInner) -> Self::Year { @@ -195,7 +198,7 @@ impl Calendar for Persian { } fn month(&self, date: &Self::DateInner) -> types::MonthInfo { - date.0.month() + self.month_code_from_ordinal(&date.0.year, date.0.month) } fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth { diff --git a/components/calendar/src/calendar.rs b/components/calendar/src/calendar.rs index 3132393fb16..2edb0e08718 100644 --- a/components/calendar/src/calendar.rs +++ b/components/calendar/src/calendar.rs @@ -6,8 +6,9 @@ use calendrical_calculations::rata_die::RataDie; use crate::cal::iso::IsoDateInner; use crate::error::DateError; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow}; -use crate::{types, DateDuration, DateDurationUnit}; +use crate::types; use core::fmt; /// A calendar implementation @@ -30,6 +31,8 @@ pub trait Calendar: crate::cal::scaffold::UnstableSealed { type DateInner: Eq + Copy + fmt::Debug; /// The type of YearInfo returned by the date type Year: fmt::Debug + Into; + /// The type of error returned by `until` + type DifferenceError; /// Construct a date from era/month codes and fields /// @@ -110,8 +113,14 @@ pub trait Calendar: crate::cal::scaffold::UnstableSealed { fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear; #[doc(hidden)] // unstable - /// Add `offset` to `date` - fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration); + /// Add `duration` to `date` + fn add( + &self, + date: &Self::DateInner, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result; + #[doc(hidden)] // unstable /// Calculate `date2 - date` as a duration /// @@ -121,10 +130,8 @@ pub trait Calendar: crate::cal::scaffold::UnstableSealed { &self, date1: &Self::DateInner, date2: &Self::DateInner, - calendar2: &Self, - largest_unit: DateDurationUnit, - smallest_unit: DateDurationUnit, - ) -> DateDuration; + options: DateDifferenceOptions, + ) -> Result; /// Returns the [`CalendarAlgorithm`](crate::preferences::CalendarAlgorithm) that is required to match /// when parsing into this calendar. diff --git a/components/calendar/src/calendar_arithmetic.rs b/components/calendar/src/calendar_arithmetic.rs index 7bb5c0151ac..1e607c5fa07 100644 --- a/components/calendar/src/calendar_arithmetic.rs +++ b/components/calendar/src/calendar_arithmetic.rs @@ -3,9 +3,10 @@ // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). use crate::error::range_check_with_overflow; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::options::{DateFromFieldsOptions, MissingFieldsStrategy, Overflow}; -use crate::types::{DateFields, DayOfYear, MonthCode}; -use crate::{types, Calendar, DateDuration, DateDurationUnit, DateError, RangeError}; +use crate::types::{DateDuration, DateDurationUnit, DateFields, DayOfYear, MonthCode}; +use crate::{types, Calendar, DateError, RangeError}; use core::cmp::Ordering; use core::convert::TryInto; use core::fmt::Debug; @@ -35,7 +36,9 @@ impl Clone for ArithmeticDate { impl PartialEq for ArithmeticDate { fn eq(&self, other: &Self) -> bool { - self.year.into() == other.year.into() && self.month == other.month && self.day == other.day + self.year.to_extended_year() == other.year.to_extended_year() + && self.month == other.month + && self.day == other.day } } @@ -44,8 +47,8 @@ impl Eq for ArithmeticDate {} impl Ord for ArithmeticDate { fn cmp(&self, other: &Self) -> Ordering { self.year - .into() - .cmp(&other.year.into()) + .to_extended_year() + .cmp(&other.year.to_extended_year()) .then(self.month.cmp(&other.month)) .then(self.day.cmp(&other.day)) } @@ -62,7 +65,7 @@ impl Hash for ArithmeticDate { where H: Hasher, { - self.year.into().hash(state); + self.year.to_extended_year().hash(state); self.month.hash(state); self.day.hash(state); } @@ -72,11 +75,17 @@ impl Hash for ArithmeticDate { #[allow(dead_code)] // TODO: Remove dead code tag after use pub(crate) const MAX_ITERS_FOR_DAYS_OF_MONTH: u8 = 33; -pub(crate) trait CalendarArithmetic: Calendar { - /// This stores the year as either an i32, or a type containing more - /// useful computational information. - type YearInfo: Copy + Debug + Into; +pub(crate) trait ToExtendedYear { + fn to_extended_year(&self) -> i32; +} + +impl ToExtendedYear for i32 { + fn to_extended_year(&self) -> i32 { + *self + } +} +pub(crate) trait CalendarArithmetic: Calendar + DateFieldsResolver { // TODO(#3933): potentially make these methods take &self instead, and absorb certain y/m parameters // based on usage patterns (e.g month_days is only ever called with self.year) fn days_in_provided_month(year: Self::YearInfo, month: u8) -> u8; @@ -128,7 +137,9 @@ pub(crate) trait CalendarArithmetic: Calendar { /// Trait for converting from era codes, month codes, and other fields to year/month/day ordinals. pub(crate) trait DateFieldsResolver: Calendar { - type YearInfo: PartialEq; + /// This stores the year as either an i32, or a type containing more + /// useful computational information. + type YearInfo: Copy + Debug + PartialEq + ToExtendedYear; /// Converts the era and era year to a YearInfo. If the calendar does not have eras, /// this should always return an Err result. @@ -164,6 +175,31 @@ pub(crate) trait DateFieldsResolver: Calendar { _ => Err(DateError::UnknownMonthCode(month_code)), } } + + /// Calculates the month code from the given ordinal month and year. + /// + /// The caller must ensure that the ordinal is in range. + /// + /// The default impl is for non-lunisolar calendars! + #[inline] + fn month_code_from_ordinal( + &self, + _year: &Self::YearInfo, + ordinal_month: u8, + ) -> types::MonthInfo { + let code = match MonthCode::new_normal(ordinal_month) { + Some(code) => code, + None => { + debug_assert!(false, "ordinal month out of range!"); + MonthCode(tinystr!(4, "und")) + } + }; + types::MonthInfo { + ordinal: ordinal_month, + standard_code: code, + formatting_code: code, + } + } } pub(crate) trait PrecomputedDataSource { @@ -209,105 +245,6 @@ impl ArithmeticDate { ArithmeticDate::new_unchecked(year, month, day) } - #[inline] - fn offset_days(&mut self, mut day_offset: i32, data: &impl PrecomputedDataSource) { - while day_offset != 0 { - let month_days = C::days_in_provided_month(self.year, self.month); - if self.day as i32 + day_offset > month_days as i32 { - self.offset_months(1, data); - day_offset -= month_days as i32; - } else if self.day as i32 + day_offset < 1 { - self.offset_months(-1, data); - day_offset += C::days_in_provided_month(self.year, self.month) as i32; - } else { - self.day = (self.day as i32 + day_offset) as u8; - day_offset = 0; - } - } - } - - #[inline] - fn offset_months( - &mut self, - mut month_offset: i32, - data: &impl PrecomputedDataSource, - ) { - while month_offset != 0 { - let year_months = C::months_in_provided_year(self.year); - if self.month as i32 + month_offset > year_months as i32 { - self.year = data.load_or_compute_info(self.year.into() + 1); - month_offset -= year_months as i32; - } else if self.month as i32 + month_offset < 1 { - self.year = data.load_or_compute_info(self.year.into() - 1); - month_offset += C::months_in_provided_year(self.year) as i32; - } else { - self.month = (self.month as i32 + month_offset) as u8; - month_offset = 0 - } - } - } - - #[inline] - pub fn offset_date( - &mut self, - offset: DateDuration, - data: &impl PrecomputedDataSource, - ) { - // TODO: THIS IS A TERRIBLE IMPL TO BE REWRITTEN - let (years, months, weeks, days) = if offset.is_negative { - ( - -(offset.years as i32), - -(offset.months as i32), - -(offset.weeks as i32), - -(offset.days as i32), - ) - } else { - ( - offset.years as i32, - offset.months as i32, - offset.weeks as i32, - offset.days as i32, - ) - }; - if years != 0 { - // For offset_date to work with lunar calendars, need to handle an edge case where the original month is not valid in the future year. - self.year = data.load_or_compute_info(self.year.into() + years); - } - - self.offset_months(months, data); - - let day_offset = days + weeks * 7 + self.day as i32 - 1; - self.day = 1; - self.offset_days(day_offset, data); - } - - #[inline] - pub fn until( - &self, - date2: ArithmeticDate, - _largest_unit: DateDurationUnit, - _smaller_unit: DateDurationUnit, - ) -> DateDuration { - // This simple implementation does not need C::PrecomputedDataSource right now, but it - // likely will once we've written a proper implementation - // TODO: THIS IS A TERRIBLE IMPL TO BE REWRITTEN - let years: i32 = self.year.into() - date2.year.into(); - let months: i32 = self.month as i32 - date2.month as i32; - let days: i64 = self.day as i64 - date2.day as i64; - let is_negative = years.is_negative() || months.is_negative() || days.is_negative(); - #[allow(clippy::panic)] - if is_negative && (years.is_positive() || months.is_positive() || days.is_positive()) { - panic!("oops, not yet supported"); - } - DateDuration { - is_negative, - years: years.unsigned_abs(), - months: months.unsigned_abs(), - weeks: 0, - days: days.unsigned_abs(), - } - } - #[inline] pub fn days_in_year(&self) -> u16 { C::days_in_provided_year(self.year) @@ -340,40 +277,7 @@ impl ArithmeticDate { } pub fn extended_year(&self) -> i32 { - self.year.into() - } - - /// The [`types::MonthInfo`] for the current month (with month code) for a solar calendar - /// Lunar calendars should not use this method and instead manually implement a month code - /// resolver. - /// Originally "solar_month" but renamed because it can be used for some lunar calendars - /// - /// Returns "und" if run with months that are out of bounds for the current - /// calendar. - #[inline] - pub fn month(&self) -> types::MonthInfo { - let code = match self.month { - a if a > C::months_in_provided_year(self.year) => tinystr!(4, "und"), - 1 => tinystr!(4, "M01"), - 2 => tinystr!(4, "M02"), - 3 => tinystr!(4, "M03"), - 4 => tinystr!(4, "M04"), - 5 => tinystr!(4, "M05"), - 6 => tinystr!(4, "M06"), - 7 => tinystr!(4, "M07"), - 8 => tinystr!(4, "M08"), - 9 => tinystr!(4, "M09"), - 10 => tinystr!(4, "M10"), - 11 => tinystr!(4, "M11"), - 12 => tinystr!(4, "M12"), - 13 => tinystr!(4, "M13"), - _ => tinystr!(4, "und"), - }; - types::MonthInfo { - ordinal: self.month, - standard_code: types::MonthCode(code), - formatting_code: types::MonthCode(code), - } + self.year.to_extended_year() } /// Construct a new arithmetic date from a year, month ordinal, and day, bounds checking @@ -413,6 +317,338 @@ impl ArithmeticDate { } } +impl ArithmeticDate { + /// Implements the Temporal abstract operation BalanceNonISODate. + /// + /// This takes a year, month, and day, where the month and day might be out of range, then + /// balances excess months into the year field and excess days into the month field. + pub(crate) fn new_balanced(year: C::YearInfo, ordinal_month: i64, day: i64, cal: &C) -> Self { + // 1. Let _resolvedYear_ be _arithmeticYear_. + // 1. Let _resolvedMonth_ be _ordinalMonth_. + let mut resolved_year = year; + let mut resolved_month = ordinal_month; + // 1. Let _monthsInYear_ be CalendarMonthsInYear(_calendar_, _resolvedYear_). + let mut months_in_year = C::months_in_provided_year(resolved_year); + // 1. Repeat, while _resolvedMonth_ ≤ 0, + // 1. Set _resolvedYear_ to _resolvedYear_ - 1. + // 1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_). + // 1. Set _resolvedMonth_ to _resolvedMonth_ + _monthsInYear_. + while resolved_month <= 0 { + resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1); + months_in_year = C::months_in_provided_year(resolved_year); + resolved_month += i64::from(months_in_year); + } + // 1. Repeat, while _resolvedMonth_ > _monthsInYear_, + // 1. Set _resolvedMonth_ to _resolvedMonth_ - _monthsInYear_. + // 1. Set _resolvedYear_ to _resolvedYear_ + 1. + // 1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_). + while resolved_month > i64::from(months_in_year) { + resolved_month -= i64::from(months_in_year); + resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1); + months_in_year = C::months_in_provided_year(resolved_year); + } + debug_assert!(u8::try_from(resolved_month).is_ok()); + let mut resolved_month = resolved_month as u8; + // 1. Let _resolvedDay_ be _day_. + let mut resolved_day = day; + // 1. Let _daysInMonth_ be CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_). + let mut days_in_month = C::days_in_provided_month(resolved_year, resolved_month); + // 1. Repeat, while _resolvedDay_ ≤ 0, + while resolved_day <= 0 { + // 1. Set _resolvedMonth_ to _resolvedMonth_ - 1. + // 1. If _resolvedMonth_ is 0, then + resolved_month -= 1; + if resolved_month == 0 { + // 1. Set _resolvedYear_ to _resolvedYear_ - 1. + // 1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_). + // 1. Set _resolvedMonth_ to _monthsInYear_. + resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() - 1); + months_in_year = C::months_in_provided_year(resolved_year); + resolved_month = months_in_year; + } + // 1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_). + // 1. Set _resolvedDay_ to _resolvedDay_ + _daysInMonth_. + days_in_month = C::days_in_provided_month(resolved_year, resolved_month); + resolved_day += i64::from(days_in_month); + } + // 1. Repeat, while _resolvedDay_ > _daysInMonth_, + while resolved_day > i64::from(days_in_month) { + // 1. Set _resolvedDay_ to _resolvedDay_ - _daysInMonth_. + // 1. Set _resolvedMonth_ to _resolvedMonth_ + 1. + // 1. If _resolvedMonth_ > _monthsInYear_, then + resolved_day -= i64::from(days_in_month); + resolved_month += 1; + if resolved_month > months_in_year { + // 1. Set _resolvedYear_ to _resolvedYear_ + 1. + // 1. Set _monthsInYear_ to CalendarMonthsInYear(_calendar_, _resolvedYear_). + // 1. Set _resolvedMonth_ to 1. + resolved_year = cal.year_info_from_extended(resolved_year.to_extended_year() + 1); + months_in_year = C::months_in_provided_year(resolved_year); + resolved_month = 1; + } + // 1. Set _daysInMonth_ to CalendarDaysInMonth(_calendar_, _resolvedYear_, _resolvedMonth_). + days_in_month = C::days_in_provided_month(resolved_year, resolved_month); + } + debug_assert!(u8::try_from(resolved_day).is_ok()); + let resolved_day = resolved_day as u8; + // 1. Return the Record { [[Year]]: _resolvedYear_, [[Month]]: _resolvedMonth_, [[Day]]: _resolvedDay_ }. + Self::new_unchecked(resolved_year, resolved_month, resolved_day) + } + + /// Implements the Temporal abstract operation NonISODateSurpasses. + /// + /// This takes two dates (`self` and `other`), `duration`, and `sign` (either -1 or 1), then + /// returns whether adding the duration to `self` results in a year/month/day that exceeds + /// `other` in the direction indicated by `sign`, constraining the month but not the day. + pub(crate) fn surpasses( + &self, + other: &Self, + duration: DateDuration, + sign: i64, + cal: &C, + ) -> bool { + // 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~)). + let base_month_code = cal + .month_code_from_ordinal(&self.year, self.month) + .standard_code; + let constrain = DateFromFieldsOptions { + overflow: Some(Overflow::Constrain), + ..Default::default() + }; + let m0_result = cal.ordinal_month_from_code(&y0, base_month_code, constrain); + let m0 = match m0_result { + Ok(m0) => m0, + Err(_) => { + debug_assert!( + false, + "valid month code for calendar, and constrained to the year" + ); + 1 + } + }; + // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _months_ + 1, 0). + let end_of_month = Self::new_balanced(y0, duration.add_months_to(m0) + 1, 0, cal); + // 1. Let _baseDay_ be _parts_.[[Day]]. + let base_day = self.day; + let y1; + let m1; + let d1; + // 1. If _weeks_ is not 0 or _days_ is not 0, then + if duration.weeks != 0 || duration.days != 0 { + // 1. If _baseDay_ < _endOfMonth_.[[Day]], then + // 1. Let _regulatedDay_ be _baseDay_. + // 1. Else, + // 1. Let _regulatedDay_ be _endOfMonth_.[[Day]]. + let regulated_day = if base_day < end_of_month.day { + base_day + } else { + end_of_month.day + }; + // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + 7 * _weeks_ + _days_). + // 1. Let _y1_ be _balancedDate_.[[Year]]. + // 1. Let _m1_ be _balancedDate_.[[Month]]. + // 1. Let _d1_ be _balancedDate_.[[Day]]. + let balanced_date = Self::new_balanced( + end_of_month.year, + i64::from(end_of_month.month), + duration.add_weeks_and_days_to(regulated_day), + cal, + ); + y1 = balanced_date.year; + m1 = balanced_date.month; + d1 = balanced_date.day; + } else { + // 1. Else, + // 1. Let _y1_ be _endOfMonth_.[[Year]]. + // 1. Let _m1_ be _endOfMonth_.[[Month]]. + // 1. Let _d1_ be _baseDay_. + y1 = end_of_month.year; + m1 = end_of_month.month; + d1 = base_day; + } + // 1. Let _calDate2_ be CalendarISOToDate(_calendar_, _toIsoDate_). + // 1. If _y1_ ≠ _calDate2_.[[Year]], then + // 1. If _sign_ × (_y1_ - _calDate2_.[[Year]]) > 0, return *true*. + // 1. Else if _m1_ ≠ _calDate2_.[[Month]], then + // 1. If _sign_ × (_m1_ - _calDate2_.[[Month]]) > 0, return *true*. + // 1. Else if _d1_ ≠ _calDate2_.[[Day]], then + // 1. If _sign_ × (_d1_ - _calDate2_.[[Day]]) > 0, return *true*. + #[allow(clippy::collapsible_if)] // to align with the spec + if y1 != other.year { + if sign * (i64::from(y1.to_extended_year()) - i64::from(other.year.to_extended_year())) + > 0 + { + return true; + } + } else if m1 != other.month { + if sign * (i64::from(m1) - i64::from(other.month)) > 0 { + return true; + } + } else if d1 != other.day { + if sign * (i64::from(d1) - i64::from(other.day)) > 0 { + return true; + } + } + // 1. Return *false*. + false + } + + /// Implements the Temporal abstract operation NonISODateAdd. + /// + /// This takes a date (`self`) and `duration`, then returns a new date resulting from + /// adding `duration` to `self`, constrained according to `options`. + pub(crate) fn added( + &self, + duration: DateDuration, + cal: &C, + options: DateAddOptions, + ) -> Result { + // 1. Let _parts_ be CalendarISOToDate(_calendar_, _isoDate_). + // 1. Let _y0_ be _parts_.[[Year]] + _duration_.[[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]], _overflow_)). + let base_month_code = cal + .month_code_from_ordinal(&self.year, self.month) + .standard_code; + let m0 = cal.ordinal_month_from_code( + &y0, + base_month_code, + DateFromFieldsOptions::from_add_options(options), + )?; + // 1. Let _endOfMonth_ be BalanceNonISODate(_calendar_, _y0_, _m0_ + _duration_.[[Months]] + 1, 0). + let end_of_month = Self::new_balanced(y0, duration.add_months_to(m0) + 1, 0, cal); + // 1. Let _baseDay_ be _parts_.[[Day]]. + let base_day = self.day; + // 1. If _baseDay_ < _endOfMonth_.[[Day]], then + // 1. Let _regulatedDay_ be _baseDay_. + let regulated_day = if base_day < end_of_month.day { + base_day + } else { + // 1. Else, + // 1. If _overflow_ is ~reject~, throw a *RangeError* exception. + // Note: ICU4X default is constrain here + if matches!(options.overflow, Some(Overflow::Reject)) { + return Err(DateError::Range { + field: "day", + value: i32::from(base_day), + min: 1, + max: i32::from(end_of_month.day), + }); + } + end_of_month.day + }; + // 1. Let _balancedDate_ be BalanceNonISODate(_calendar_, _endOfMonth_.[[Year]], _endOfMonth_.[[Month]], _regulatedDay_ + 7 * _duration_.[[Weeks]] + _duration_.[[Days]]). + // 1. Let _result_ be ? CalendarIntegersToISO(_calendar_, _balancedDate_.[[Year]], _balancedDate_.[[Month]], _balancedDate_.[[Day]]). + // 1. Return _result_. + Ok(Self::new_balanced( + end_of_month.year, + i64::from(end_of_month.month), + duration.add_weeks_and_days_to(regulated_day), + cal, + )) + } + + /// Implements the Temporal abstract operation NonISODateUntil. + /// + /// This takes a duration (`self`) and a date (`other`), then returns a duration that, when + /// added to `self`, results in `other`, with largest unit according to `options`. + pub(crate) fn until( + &self, + other: &Self, + cal: &C, + options: DateDifferenceOptions, + ) -> DateDuration { + // 1. Let _sign_ be -1 × CompareISODate(_one_, _two_). + // 1. If _sign_ = 0, return ZeroDateDuration(). + let sign = match other.cmp(self) { + Ordering::Greater => 1i64, + Ordering::Equal => return DateDuration::default(), + Ordering::Less => -1i64, + }; + // 1. Let _years_ be 0. + // 1. If _largestUnit_ is ~year~, then + // 1. Let _candidateYears_ be _sign_. + // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _candidateYears_, 0, 0, 0) is *false*, + // 1. Set _years_ to _candidateYears_. + // 1. Set _candidateYears_ to _candidateYears_ + _sign_. + let mut years = 0; + if matches!(options.largest_unit, Some(DateDurationUnit::Years)) { + let mut candidate_years = sign; + while !self.surpasses( + other, + DateDuration::from_signed_ymwd(candidate_years, 0, 0, 0), + sign, + cal, + ) { + years = candidate_years; + candidate_years += sign; + } + } + // 1. Let _months_ be 0. + // 1. If _largestUnit_ is ~year~ or _largestUnit_ is ~month~, then + // 1. Let _candidateMonths_ be _sign_. + // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _candidateMonths_, 0, 0) is *false*, + // 1. Set _months_ to _candidateMonths_. + // 1. Set _candidateMonths_ to _candidateMonths_ + _sign_. + let mut months = 0; + if matches!( + options.largest_unit, + Some(DateDurationUnit::Years) | Some(DateDurationUnit::Months) + ) { + let mut candidate_months = sign; + while !self.surpasses( + other, + DateDuration::from_signed_ymwd(years, candidate_months, 0, 0), + sign, + cal, + ) { + months = candidate_months; + candidate_months += sign; + } + } + // 1. Let _weeks_ be 0. + // 1. If _largestUnit_ is ~week~, then + // 1. Let _candidateWeeks_ be _sign_. + // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _candidateWeeks_, 0) is *false*, + // 1. Set _weeks_ to _candidateWeeks_. + // 1. Set _candidateWeeks_ to _candidateWeeks_ + sign. + let mut weeks = 0; + if matches!(options.largest_unit, Some(DateDurationUnit::Weeks)) { + let mut candidate_weeks = sign; + while !self.surpasses( + other, + DateDuration::from_signed_ymwd(years, months, candidate_weeks, 0), + sign, + cal, + ) { + weeks = candidate_weeks; + candidate_weeks += sign; + } + } + // 1. Let _days_ be 0. + // 1. Let _candidateDays_ be _sign_. + // 1. Repeat, while NonISODateSurpasses(_calendar_, _sign_, _one_, _two_, _years_, _months_, _weeks_, _candidateDays_) is *false*, + // 1. Set _days_ to _candidateDays_. + // 1. Set _candidateDays_ to _candidateDays_ + _sign_. + let mut days = 0; + let mut candidate_days = sign; + while !self.surpasses( + other, + DateDuration::from_signed_ymwd(years, months, weeks, candidate_days), + sign, + cal, + ) { + days = candidate_days; + candidate_days += sign; + } + // 1. Return ! CreateDateDurationRecord(_years_, _months_, _weeks_, _days_). + DateDuration::from_signed_ymwd(years, months, weeks, days) + } +} + pub(crate) struct ArithmeticDateBuilder { pub(crate) year: YearInfo, pub(crate) month: u8, diff --git a/components/calendar/src/date.rs b/components/calendar/src/date.rs index 25ec59b600b..374cf5d9a42 100644 --- a/components/calendar/src/date.rs +++ b/components/calendar/src/date.rs @@ -7,9 +7,10 @@ use crate::cal::{abstract_gregorian::AbstractGregorian, iso::IsoEra}; use crate::calendar_arithmetic::CalendarArithmetic; use crate::error::DateError; use crate::options::DateFromFieldsOptions; +use crate::options::{DateAddOptions, DateDifferenceOptions}; use crate::types::{CyclicYear, EraYear, IsoWeekOfYear}; use crate::week::{RelativeUnit, WeekCalculator, WeekOf}; -use crate::{types, Calendar, DateDuration, DateDurationUnit, Iso}; +use crate::{types, Calendar, Iso}; #[cfg(feature = "alloc")] use alloc::rc::Rc; #[cfg(feature = "alloc")] @@ -241,36 +242,66 @@ impl Date { /// Add a `duration` to this date, mutating it #[doc(hidden)] // unstable #[inline] - pub fn add(&mut self, duration: DateDuration) { - self.calendar + pub fn try_add_with_options( + &mut self, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result<(), DateError> { + let inner = self + .calendar .as_calendar() - .offset_date(&mut self.inner, duration) + .add(&self.inner, duration, options)?; + self.inner = inner; + Ok(()) } /// Add a `duration` to this date, returning the new one #[doc(hidden)] // unstable #[inline] - pub fn added(mut self, duration: DateDuration) -> Self { - self.add(duration); - self + pub fn try_added_with_options( + mut self, + duration: types::DateDuration, + options: DateAddOptions, + ) -> Result { + self.try_add_with_options(duration, options)?; + Ok(self) } /// Calculating the duration between `other - self` + /// + /// Although this returns a [`Result`], with most fixed calendars, this operation can't fail. + /// In such cases, the error type is [`Infallible`], and the inner value can be safely + /// unwrapped using [`Result::into_ok()`], which is available in nightly Rust as of this + /// writing. In stable Rust, the value can be unwrapped using [pattern matching]. + /// + /// # Examples + /// + /// ``` + /// use icu::calendar::Date; + /// use icu::calendar::types::DateDuration; + /// + /// let d1 = Date::try_new_iso(2020, 1, 1).unwrap(); + /// let d2 = Date::try_new_iso(2025, 10, 2).unwrap(); + /// let options = Default::default(); + /// + /// // The value can be unwrapped with destructuring syntax: + /// let Ok(duration) = d1.try_until_with_options(&d2, options); + /// + /// assert_eq!(duration, DateDuration::for_days(2101)); + /// ``` + /// + /// [`Infallible`]: core::convert::Infallible + /// [pattern matching]: https://doc.rust-lang.org/book/ch19-03-pattern-syntax.html #[doc(hidden)] // unstable #[inline] - pub fn until>( + pub fn try_until_with_options>( &self, other: &Date, - largest_unit: DateDurationUnit, - smallest_unit: DateDurationUnit, - ) -> DateDuration { - self.calendar.as_calendar().until( - self.inner(), - other.inner(), - other.calendar.as_calendar(), - largest_unit, - smallest_unit, - ) + options: DateDifferenceOptions, + ) -> Result::DifferenceError> { + self.calendar + .as_calendar() + .until(self.inner(), other.inner(), options) } /// The calendar-specific year-info. diff --git a/components/calendar/src/duration.rs b/components/calendar/src/duration.rs index 4273bccf43f..f2aa59901f2 100644 --- a/components/calendar/src/duration.rs +++ b/components/calendar/src/duration.rs @@ -14,7 +14,11 @@ /// # Example /// /// ```rust -/// use icu::calendar::{types::Weekday, Date, DateDuration, DateDurationUnit}; +/// use icu::calendar::types::Weekday; +/// use icu::calendar::options::DateDifferenceOptions; +/// use icu::calendar::Date; +/// use icu::calendar::types::DateDuration; +/// use icu::calendar::types::DateDurationUnit; /// /// // Creating ISO date: 1992-09-02. /// let mut date_iso = Date::try_new_iso(1992, 9, 2) @@ -30,25 +34,25 @@ /// assert_eq!(date_iso.days_in_month(), 30); /// /// // Advancing date in-place by 1 year, 2 months, 3 weeks, 4 days. -/// date_iso.add(DateDuration { +/// date_iso.try_add_with_options(DateDuration { /// is_negative: false, /// years: 1, /// months: 2, /// weeks: 3, /// days: 4 -/// }); +/// }, Default::default()).unwrap(); /// assert_eq!(date_iso.era_year().year, 1993); /// assert_eq!(date_iso.month().ordinal, 11); /// assert_eq!(date_iso.day_of_month().0, 27); /// /// // Reverse date advancement. -/// date_iso.add(DateDuration { +/// date_iso.try_add_with_options(DateDuration { /// is_negative: true, /// years: 1, /// months: 2, /// weeks: 3, /// days: 4 -/// }); +/// }, Default::default()).unwrap(); /// assert_eq!(date_iso.era_year().year, 1992); /// assert_eq!(date_iso.month().ordinal, 9); /// assert_eq!(date_iso.day_of_month().0, 2); @@ -58,23 +62,24 @@ /// .expect("Failed to initialize ISO Date instance."); /// /// // Comparing dates: 2022-01-30 and 1992-09-02. -/// let duration = newer_date_iso.until( +/// let mut options = DateDifferenceOptions::default(); +/// options.largest_unit = Some(DateDurationUnit::Years); +/// let Ok(duration) = newer_date_iso.try_until_with_options( /// &date_iso, -/// DateDurationUnit::Years, -/// DateDurationUnit::Days, +/// options, /// ); /// assert_eq!(duration.years, 30); /// assert_eq!(duration.months, 1); /// assert_eq!(duration.days, 28); /// /// // Create new date with date advancement. Reassign to new variable. -/// let mutated_date_iso = date_iso.added(DateDuration { +/// let mutated_date_iso = date_iso.try_added_with_options(DateDuration { /// is_negative: false, /// years: 1, /// months: 2, /// weeks: 3, /// days: 4 -/// }); +/// }, Default::default()).unwrap(); /// assert_eq!(mutated_date_iso.era_year().year, 1993); /// assert_eq!(mutated_date_iso.month().ordinal, 11); /// assert_eq!(mutated_date_iso.day_of_month().0, 27); @@ -157,4 +162,98 @@ impl DateDuration { ..Default::default() } } + + /// Do NOT pass this function values of mixed signs! + pub(crate) fn from_signed_ymwd(years: i64, months: i64, weeks: i64, days: i64) -> Self { + let is_negative = years.is_negative() + || months.is_negative() + || weeks.is_negative() + || days.is_negative(); + if is_negative + && (years.is_positive() + || months.is_positive() + || weeks.is_positive() + || days.is_positive()) + { + debug_assert!(false, "mixed signs in from_signed_ymd"); + } + Self { + is_negative, + years: match u32::try_from(years.unsigned_abs()) { + Ok(x) => x, + Err(_) => { + debug_assert!(false, "years out of range"); + u32::MAX + } + }, + months: match u32::try_from(months.unsigned_abs()) { + Ok(x) => x, + Err(_) => { + debug_assert!(false, "months out of range"); + u32::MAX + } + }, + weeks: match u32::try_from(weeks.unsigned_abs()) { + Ok(x) => x, + Err(_) => { + debug_assert!(false, "weeks out of range"); + u32::MAX + } + }, + days: days.unsigned_abs(), + } + } + + #[inline] + pub(crate) fn add_years_to(&self, year: i32) -> i32 { + if !self.is_negative { + match year.checked_add_unsigned(self.years) { + Some(x) => x, + None => { + debug_assert!(false, "{year} + {self:?} out of year range"); + i32::MAX + } + } + } else { + match year.checked_sub_unsigned(self.years) { + Some(x) => x, + None => { + debug_assert!(false, "{year} - {self:?} out of year range"); + i32::MIN + } + } + } + } + + #[inline] + pub(crate) fn add_months_to(&self, month: u8) -> i64 { + if !self.is_negative { + i64::from(month) + i64::from(self.months) + } else { + i64::from(month) - i64::from(self.months) + } + } + + #[inline] + pub(crate) fn add_weeks_and_days_to(&self, day: u8) -> i64 { + if !self.is_negative { + let day = i64::from(day) + i64::from(self.weeks) * 7; + match day.checked_add_unsigned(self.days) { + Some(x) => x, + None => { + debug_assert!(false, "{day} + {self:?} out of day range"); + i64::MAX + } + } + } else { + let day = i64::from(day) - i64::from(self.weeks) * 7; + match day.checked_sub_unsigned(self.days) { + Some(x) => x, + None => { + debug_assert!(false, "{day} - {self:?} out of day range"); + i64::MIN + } + } + } + } } diff --git a/components/calendar/src/lib.rs b/components/calendar/src/lib.rs index b18e9783ac1..3854a662174 100644 --- a/components/calendar/src/lib.rs +++ b/components/calendar/src/lib.rs @@ -114,8 +114,6 @@ mod ixdtf; pub use any_calendar::IntoAnyCalendar; pub use calendar::Calendar; pub use date::{AsCalendar, Date, Ref}; -#[doc(hidden)] // unstable -pub use duration::{DateDuration, DateDurationUnit}; pub use error::{DateError, RangeError}; #[cfg(feature = "ixdtf")] pub use ixdtf::ParseError; diff --git a/components/calendar/src/options.rs b/components/calendar/src/options.rs index 3976e060ef2..13c6ae35481 100644 --- a/components/calendar/src/options.rs +++ b/components/calendar/src/options.rs @@ -4,16 +4,209 @@ //! Options used by types in this crate +use crate::types; + /// Options bag for [`Date::try_from_fields`](crate::Date::try_from_fields). #[derive(Copy, Clone, Debug, PartialEq, Default)] #[non_exhaustive] pub struct DateFromFieldsOptions { /// How to behave with out-of-bounds fields. + /// + /// Defaults to [`Overflow::Reject`]. + /// + /// # Examples + /// + /// ``` + /// use icu::calendar::Date; + /// use icu::calendar::types::DateFields; + /// use icu::calendar::options::Overflow; + /// use icu::calendar::options::DateFromFieldsOptions; + /// use icu::calendar::Iso; + /// + /// // There is no day 31 in September. + /// let mut fields = DateFields::default(); + /// fields.extended_year = Some(2025); + /// fields.ordinal_month = core::num::NonZero::new(9); + /// fields.day = core::num::NonZero::new(31); + /// + /// let options_default = DateFromFieldsOptions::default(); + /// assert!(Date::try_from_fields(fields, options_default, Iso).is_err()); + /// + /// let mut options_reject = options_default.clone(); + /// options_reject.overflow = Some(Overflow::Reject); + /// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err()); + /// + /// let mut options_constrain = options_default.clone(); + /// options_constrain.overflow = Some(Overflow::Constrain); + /// assert_eq!( + /// Date::try_from_fields(fields, options_constrain, Iso).unwrap(), + /// Date::try_new_iso(2025, 9, 30).unwrap() + /// ); + /// ``` pub overflow: Option, /// How to behave when the fields that are present do not fully constitute a Date. + /// + /// This option can be used to fill in a missing year given a month and a day according to + /// the ECMAScript Temporal specification. + /// + /// # Examples + /// + /// ``` + /// use icu::calendar::Date; + /// use icu::calendar::types::DateFields; + /// use icu::calendar::types::MonthCode; + /// use icu::calendar::options::MissingFieldsStrategy; + /// use icu::calendar::options::DateFromFieldsOptions; + /// use icu::calendar::Iso; + /// + /// // These options are missing a year. + /// let mut fields = DateFields::default(); + /// fields.month_code = MonthCode::new_normal(2); + /// fields.day = core::num::NonZero::new(1); + /// + /// let options_default = DateFromFieldsOptions::default(); + /// assert!(Date::try_from_fields(fields, options_default, Iso).is_err()); + /// + /// let mut options_reject = options_default.clone(); + /// options_reject.missing_fields_strategy = Some(MissingFieldsStrategy::Reject); + /// assert!(Date::try_from_fields(fields, options_reject, Iso).is_err()); + /// + /// let mut options_ecma = options_default.clone(); + /// options_ecma.missing_fields_strategy = Some(MissingFieldsStrategy::Ecma); + /// assert_eq!( + /// Date::try_from_fields(fields, options_ecma, Iso).unwrap(), + /// Date::try_new_iso(1972, 2, 1).unwrap() + /// ); + /// ``` pub missing_fields_strategy: Option, } +impl DateFromFieldsOptions { + pub(crate) fn from_add_options(options: DateAddOptions) -> Self { + Self { + overflow: options.overflow, + missing_fields_strategy: Default::default(), + } + } +} + +/// Options for adding a duration to a date. +#[derive(Copy, Clone, PartialEq, Debug, Default)] +#[non_exhaustive] +#[doc(hidden)] // unstable +pub struct DateAddOptions { + /// How to behave with out-of-bounds fields during arithmetic. + /// + /// Defaults to [`Overflow::Constrain`]. + /// + /// # Examples + /// + /// ``` + /// use icu::calendar::Date; + /// use icu::calendar::types::DateDuration; + /// use icu::calendar::options::DateAddOptions; + /// use icu::calendar::options::Overflow; + /// + /// // There is a day 31 in October but not in November. + /// let d1 = Date::try_new_iso(2025, 10, 31).unwrap(); + /// let duration = DateDuration::for_months(1); + /// + /// let options_default = DateAddOptions::default(); + /// assert_eq!( + /// d1.try_added_with_options(duration, options_default).unwrap(), + /// Date::try_new_iso(2025, 11, 30).unwrap() + /// ); + /// + /// let mut options_reject = options_default.clone(); + /// options_reject.overflow = Some(Overflow::Reject); + /// assert!(d1.try_added_with_options(duration, options_reject).is_err()); + /// + /// let mut options_constrain = options_default.clone(); + /// options_constrain.overflow = Some(Overflow::Constrain); + /// assert_eq!( + /// d1.try_added_with_options(duration, options_constrain).unwrap(), + /// Date::try_new_iso(2025, 11, 30).unwrap() + /// ); + /// ``` + pub overflow: Option, +} + +/// Options for taking the difference between two dates. +#[derive(Copy, Clone, PartialEq, Debug, Default)] +#[non_exhaustive] +#[doc(hidden)] // unstable +pub struct DateDifferenceOptions { + /// Which date field to allow as the largest in a duration when taking the difference. + /// + /// When choosing [`Months`] or [`Years`], the resulting [`DateDuration`] might not be + /// associative or commutative in subsequent arithmetic operations, and it might require + /// [`Overflow::Constrain`] in addition. + /// + /// # Examples + /// + /// ``` + /// use icu::calendar::Date; + /// use icu::calendar::types::DateDuration; + /// use icu::calendar::types::DateDurationUnit; + /// use icu::calendar::options::DateDifferenceOptions; + /// + /// let d1 = Date::try_new_iso(2025, 3, 31).unwrap(); + /// let d2 = Date::try_new_iso(2026, 5, 15).unwrap(); + /// + /// let options_default = DateDifferenceOptions::default(); + /// assert_eq!( + /// d1.try_until_with_options(&d2, options_default).unwrap(), + /// DateDuration::for_days(410) + /// ); + /// + /// let mut options_days = options_default.clone(); + /// options_days.largest_unit = Some(DateDurationUnit::Days); + /// assert_eq!( + /// d1.try_until_with_options(&d2, options_default).unwrap(), + /// DateDuration::for_days(410) + /// ); + /// + /// let mut options_weeks = options_default.clone(); + /// options_weeks.largest_unit = Some(DateDurationUnit::Weeks); + /// assert_eq!( + /// d1.try_until_with_options(&d2, options_weeks).unwrap(), + /// DateDuration { + /// weeks: 58, + /// days: 4, + /// ..Default::default() + /// } + /// ); + /// + /// let mut options_months = options_default.clone(); + /// options_months.largest_unit = Some(DateDurationUnit::Months); + /// assert_eq!( + /// d1.try_until_with_options(&d2, options_months).unwrap(), + /// DateDuration { + /// months: 13, + /// days: 15, + /// ..Default::default() + /// } + /// ); + /// + /// let mut options_years = options_default.clone(); + /// options_years.largest_unit = Some(DateDurationUnit::Years); + /// assert_eq!( + /// d1.try_until_with_options(&d2, options_years).unwrap(), + /// DateDuration { + /// years: 1, + /// months: 1, + /// days: 15, + /// ..Default::default() + /// } + /// ); + /// ``` + /// + /// [`Months`]: types::DateDurationUnit::Months + /// [`Years`]: types::DateDurationUnit::Years + /// [`DateDuration`]: types::DateDuration + pub largest_unit: Option, +} + /// Whether to constrain or reject out-of-bounds values when constructing a Date. /// /// The behavior conforms to the ECMAScript Temporal specification. @@ -79,7 +272,6 @@ pub enum Overflow { /// assert_eq!(date.month().standard_code, MonthCode(tinystr!(4, "M06"))); /// assert_eq!(date.day_of_month().0, 29); /// ``` - #[default] Constrain, /// Return an error if any fields are out of bounds. /// @@ -132,6 +324,7 @@ pub enum Overflow { /// .expect_err("Month is out of bounds"); /// assert!(matches!(err, DateError::UnknownMonthCode(_))); /// ``` + #[default] Reject, } diff --git a/components/calendar/src/tests/continuity_test.rs b/components/calendar/src/tests/continuity_test.rs index 0971bdded89..63e6c7fb50e 100644 --- a/components/calendar/src/tests/continuity_test.rs +++ b/components/calendar/src/tests/continuity_test.rs @@ -2,9 +2,9 @@ // called LICENSE at the top level of the ICU4X source tree // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). -use crate::*; +use crate::{types::*, *}; -fn check_continuity(mut date: Date) { +fn check_continuity(mut date: Date, years_to_check: usize) { let duration = DateDuration::for_days(1); let mut rata_die = date.to_rata_die(); @@ -12,8 +12,10 @@ fn check_continuity(mut date: Date) { let mut year = date.year(); let mut is_in_leap_year = date.is_in_leap_year(); - for _ in 0..(366 * 20) { - let next_date = date.added(duration); + for _ in 0..(366 * years_to_check) { + let next_date = date + .try_added_with_options(duration, Default::default()) + .unwrap(); let next_rata_die = next_date.to_iso().to_rata_die(); assert_eq!(next_rata_die, rata_die + 1, "{next_date:?}"); let next_weekday = next_date.day_of_week(); @@ -35,13 +37,15 @@ fn check_continuity(mut date: Date) { } } -fn check_every_250_days(mut date: Date) { +fn check_every_250_days(mut date: Date, iters: usize) { let duration = DateDuration::for_days(250); let mut rata_die = date.to_rata_die(); - for _ in 0..2000 { - let next_date = date.added(duration); + for _ in 0..iters { + let next_date = date + .try_added_with_options(duration, Default::default()) + .unwrap(); let next_iso = next_date.to_iso(); let next_rata_die = next_iso.to_rata_die(); assert_eq!(next_rata_die, rata_die + 250, "{next_date:?}"); @@ -55,91 +59,91 @@ fn check_every_250_days(mut date: Date) { #[test] fn test_buddhist_continuity() { let date = Date::try_new_buddhist(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_buddhist(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_chinese_continuity() { let cal = crate::cal::LunarChinese::new_china(); let date = Date::try_new_chinese_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_chinese_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); let date = Date::try_new_chinese_with_calendar(-10000, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); let date = Date::try_new_chinese_with_calendar(1899, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_chinese_with_calendar(2099, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); } #[test] fn test_coptic_continuity() { let date = Date::try_new_coptic(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_coptic(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_korean_continuity() { let cal = crate::cal::LunarChinese::new_korea(); let date = Date::try_new_chinese_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_chinese_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); let date = Date::try_new_chinese_with_calendar(1900, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_chinese_with_calendar(2100, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); } #[test] fn test_ethiopian_continuity() { use crate::cal::EthiopianEraStyle::*; let date = Date::try_new_ethiopian(AmeteMihret, -10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_ethiopian(AmeteMihret, -300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_ethiopian_amete_alem_continuity() { use crate::cal::EthiopianEraStyle::*; let date = Date::try_new_ethiopian(AmeteAlem, -10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_ethiopian(AmeteAlem, -300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_gregorian_continuity() { let date = Date::try_new_gregorian(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_gregorian(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_hebrew_continuity() { let date = Date::try_new_hebrew(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hebrew(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_indian_continuity() { let date = Date::try_new_indian(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_indian(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] @@ -149,9 +153,9 @@ fn test_hijri_civil_continuity() { crate::cal::hijri::TabularAlgorithmEpoch::Friday, ); let date = Date::try_new_hijri_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hijri_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] @@ -160,9 +164,11 @@ fn test_hijri_simulated_mecca_continuity() { let _ = simple_logger::SimpleLogger::new().env().init(); let cal = crate::cal::Hijri::new_simulated_mecca(); let date = Date::try_new_hijri_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + // This test is slow since it is doing astronomical calculations, so check only 3 years + check_continuity(date.unwrap(), 3); let date = Date::try_new_hijri_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + // This test is slow since it is doing astronomical calculations, so check only 100 dates + check_every_250_days(date.unwrap(), 100); } #[test] @@ -172,9 +178,9 @@ fn test_hijri_tabular_continuity() { crate::cal::hijri::TabularAlgorithmEpoch::Thursday, ); let date = Date::try_new_hijri_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hijri_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] @@ -183,21 +189,21 @@ fn test_hijri_umm_al_qura_continuity() { let _ = simple_logger::SimpleLogger::new().env().init(); let cal = crate::cal::Hijri::new_umm_al_qura(); let date = Date::try_new_hijri_with_calendar(-10, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hijri_with_calendar(1290, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hijri_with_calendar(1590, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_hijri_with_calendar(-300, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_iso_continuity() { let date = Date::try_new_iso(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_iso(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] @@ -205,9 +211,9 @@ fn test_japanese_continuity() { let cal = crate::cal::Japanese::new(); let cal = Ref(&cal); let date = Date::try_new_japanese_with_calendar("heisei", 20, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_japanese_with_calendar("bce", 500, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] @@ -215,23 +221,23 @@ fn test_japanese_extended_continuity() { let cal = crate::cal::JapaneseExtended::new(); let cal = Ref(&cal); let date = Date::try_new_japanese_extended_with_calendar("heisei", 20, 1, 1, cal); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_japanese_extended_with_calendar("bce", 500, 1, 1, cal); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_persian_continuity() { let date = Date::try_new_persian(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_persian(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } #[test] fn test_roc_continuity() { let date = Date::try_new_roc(-10, 1, 1); - check_continuity(date.unwrap()); + check_continuity(date.unwrap(), 20); let date = Date::try_new_roc(-300, 1, 1); - check_every_250_days(date.unwrap()); + check_every_250_days(date.unwrap(), 2000); } diff --git a/components/calendar/src/types.rs b/components/calendar/src/types.rs index c919f218ac2..aa1e07044ff 100644 --- a/components/calendar/src/types.rs +++ b/components/calendar/src/types.rs @@ -12,6 +12,10 @@ use tinystr::TinyAsciiStr; use tinystr::{TinyStr16, TinyStr4}; use zerovec::ule::AsULE; +// Export the duration types from here +#[doc(hidden)] // unstable +pub use crate::duration::{DateDuration, DateDurationUnit}; + /// A bag of various ways of expressing the year, month, and/or day. /// /// Pass this into [`Date::try_from_fields`](crate::Date::try_from_fields). diff --git a/components/calendar/src/week.rs b/components/calendar/src/week.rs index f6187e46a4c..e835fa1c811 100644 --- a/components/calendar/src/week.rs +++ b/components/calendar/src/week.rs @@ -294,7 +294,7 @@ impl Iterator for WeekdaySetIterator { #[cfg(test)] mod tests { use super::*; - use crate::{types::Weekday, Date, DateDuration, RangeError}; + use crate::{types::DateDuration, types::Weekday, Date, RangeError}; static ISO_CALENDAR: WeekCalculator = WeekCalculator { first_weekday: Weekday::Monday, @@ -457,11 +457,9 @@ mod tests { let day = (yyyymmdd % 100) as u8; let date = Date::try_new_iso(year, month, day)?; - let previous_month = date.added(DateDuration { - months: 1, - is_negative: true, - ..Default::default() - }); + let previous_month = date + .try_added_with_options(DateDuration::for_months(-1), Default::default()) + .unwrap(); calendar.week_of( u16::from(previous_month.days_in_month()), diff --git a/components/calendar/tests/arithmetic.rs b/components/calendar/tests/arithmetic.rs new file mode 100644 index 00000000000..4ab26336ee5 --- /dev/null +++ b/components/calendar/tests/arithmetic.rs @@ -0,0 +1,239 @@ +// This file is part of ICU4X. For terms of use, please see the file +// called LICENSE at the top level of the ICU4X source tree +// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ). + +use std::convert::Infallible; + +use icu_calendar::{ + cal::Hebrew, + options::{DateAddOptions, DateDifferenceOptions, Overflow}, + types::{DateDuration, DateDurationUnit, MonthCode}, + AsCalendar, Calendar, Date, Iso, +}; + +#[rustfmt::skip] +#[allow(clippy::type_complexity)] +const ISO_DATE_PAIRS: &[(&str, &str, u64, (u32, u64), (u32, u64), (u32, u32, u64))] = &[ + // d0, d1, D, (W, D), (M, D), (Y, M, D) + ("2020-01-03", "2020-02-15", 43, (6, 1), (1, 12), (0, 1, 12)), + ("2020-01-31", "2020-06-30", 151, (21, 4), (4, 30), (0, 4, 30)), + ("2020-03-31", "2020-07-30", 121, (17, 2), (3, 30), (0, 3, 30)), + ("2020-03-31", "2020-07-31", 122, (17, 3), (4, 0), (0, 4, 0)), + ("2016-03-20", "2020-03-05", 1446, (206, 4), (47, 14), (3, 11, 14)), + ("2020-02-29", "2022-03-01", 731, (104, 3), (24, 1), (2, 0, 1)), + + // Negative direction: + ("2020-02-15", "2020-01-03", 43, (6, 1), (1, 12), (0, 1, 12)), + ("2020-06-30", "2020-01-31", 151, (21, 4), (4, 29), (0, 4, 29)), // DIFF +/- + ("2020-07-30", "2020-03-31", 121, (17, 2), (3, 30), (0, 3, 30)), + ("2020-07-31", "2020-03-31", 122, (17, 3), (4, 0), (0, 4, 0)), + ("2020-03-05", "2016-03-20", 1446, (206, 4), (47, 16), (3, 11, 16)), // DIFF +/- + ("2022-03-01", "2020-02-29", 731, (104, 3), (24, 1), (2, 0, 1)), +]; + +fn check( + d0: &Date, + d1: &Date, + exp0: &u64, + exp1: &(u32, u64), + exp2: &(u32, u64), + exp3: &(u32, u32, u64), +) where + A: AsCalendar + Copy, + ::Calendar: Calendar, + <::Calendar as Calendar>::DateInner: PartialOrd, +{ + let is_negative = d0 > d1; + let mut add_options = DateAddOptions::default(); + add_options.overflow = Some(Overflow::Constrain); + let mut until_options0 = DateDifferenceOptions::default(); + until_options0.largest_unit = Some(DateDurationUnit::Days); + let mut until_options1 = DateDifferenceOptions::default(); + until_options1.largest_unit = Some(DateDurationUnit::Weeks); + let mut until_options2 = DateDifferenceOptions::default(); + until_options2.largest_unit = Some(DateDurationUnit::Months); + let mut until_options3 = DateDifferenceOptions::default(); + until_options3.largest_unit = Some(DateDurationUnit::Years); + + let Ok(p0) = d0.try_until_with_options(d1, until_options0); + assert_eq!( + p0, + DateDuration { + is_negative, + days: *exp0, + ..Default::default() + }, + "{d0:?}/{d1:?}" + ); + assert_eq!( + d0.try_added_with_options(p0, add_options).unwrap(), + *d1, + "{d0:?}/{d1:?}" + ); + + let Ok(p1) = d0.try_until_with_options(d1, until_options1); + assert_eq!( + p1, + DateDuration { + is_negative, + weeks: exp1.0, + days: exp1.1, + ..Default::default() + }, + "{d0:?}/{d1:?}" + ); + assert_eq!( + d0.try_added_with_options(p1, add_options).unwrap(), + *d1, + "{d0:?}/{d1:?}" + ); + + let Ok(p2) = d0.try_until_with_options(d1, until_options2); + assert_eq!( + p2, + DateDuration { + is_negative, + months: exp2.0, + days: exp2.1, + ..Default::default() + }, + "{d0:?}/{d1:?}" + ); + assert_eq!( + d0.try_added_with_options(p2, add_options).unwrap(), + *d1, + "{d0:?}/{d1:?}" + ); + + let Ok(p3) = d0.try_until_with_options(d1, until_options3); + assert_eq!( + p3, + DateDuration { + is_negative, + years: exp3.0, + months: exp3.1, + days: exp3.2, + ..Default::default() + }, + "{d0:?}/{d1:?}" + ); + assert_eq!( + d0.try_added_with_options(p3, add_options).unwrap(), + *d1, + "{d0:?}/{d1:?}" + ); + + // RataDie addition should be equivalent for largest unit Days and Weeks + let rd_diff = d1.to_rata_die() - d0.to_rata_die(); + if is_negative { + assert!(rd_diff.is_negative()); + } + assert_eq!(p0.days, rd_diff.unsigned_abs()); + assert_eq!(p1.days + u64::from(p1.weeks) * 7, rd_diff.unsigned_abs()); +} + +#[test] +fn test_arithmetic_cases() { + for (d0, d1, exp0, exp1, exp2, exp3) in ISO_DATE_PAIRS { + let d0 = Date::try_from_str(d0, Iso).unwrap(); + let d1 = Date::try_from_str(d1, Iso).unwrap(); + check(&d0, &d1, exp0, exp1, exp2, exp3); + } +} + +#[test] +fn test_hebrew() { + let m06z_20 = Date::try_new_hebrew(5783, 6, 20).unwrap(); + let m05l_15 = Date::try_new_hebrew(5784, 6, 15).unwrap(); + let m05l_30 = Date::try_new_hebrew(5784, 6, 30).unwrap(); + let m06a_29 = Date::try_new_hebrew(5784, 7, 29).unwrap(); + let m07a_10 = Date::try_new_hebrew(5784, 8, 10).unwrap(); + let m06b_15 = Date::try_new_hebrew(5785, 6, 15).unwrap(); + let m07b_20 = Date::try_new_hebrew(5785, 7, 20).unwrap(); + + #[rustfmt::skip] + #[allow(clippy::type_complexity)] + let cases: &[(&Date, &Date, u64, (u32, u64), (u32, u64), (u32, u32, u64))] = &[ + (&m06z_20, &m05l_15, 348, (49, 5), (11, 25), (0, 11, 25)), + (&m06z_20, &m05l_30, 363, (51, 6), (12, 10), (0, 12, 10)), + (&m06z_20, &m06a_29, 392, (56, 0), (13, 9), (1, 0, 9)), + (&m06z_20, &m07a_10, 402, (57, 3), (13, 19), (1, 0, 19)), + (&m06z_20, &m06b_15, 733, (104,5), (24, 25), (1, 11, 25)), + (&m06z_20, &m07b_20, 767, (109,4), (26, 0), (2, 1, 0)), + + (&m05l_15, &m05l_30, 15, (2, 1), (0, 15), (0, 0, 15)), + (&m05l_15, &m06a_29, 44, (6, 2), (1, 14), (0, 1, 14)), + (&m05l_15, &m07a_10, 54, (7, 5), (1, 24), (0, 1, 24)), + (&m05l_15, &m06b_15, 385, (55, 0), (13, 0), (1, 0, 0)), // M05L to M06 common year + (&m05l_15, &m07b_20, 419, (59, 6), (14, 5), (1, 1, 5)), // M05L to M07 common year + + (&m05l_30, &m06a_29, 29, (4, 1), (0, 29), (0, 0, 29)), + (&m05l_30, &m07a_10, 39, (5, 4), (1, 10), (0, 1, 10)), + (&m05l_30, &m06b_15, 370, (52, 6), (12, 15), (0, 12, 15)), // M05L to M06 common year + (&m05l_30, &m07b_20, 404, (57, 5), (13, 20), (1, 0, 20)), // M05L to M07 common year + + (&m06a_29, &m07a_10, 10, (1, 3), (0, 10), (0, 0, 10)), + (&m06a_29, &m06b_15, 341, (48, 5), (11, 16), (0, 11, 16)), // M06 leap year to M06 common year + (&m06a_29, &m07b_20, 375, (53, 4), (12, 20), (1, 0, 20)), // M06 leap year to M07 common year + (&m07a_10, &m06b_15, 331, (47, 2), (11, 5), (0, 11, 5)), // M07 leap year to M06 common year + (&m07a_10, &m07b_20, 365, (52, 1), (12, 10), (1, 0, 10)), // M07 leap year to M06 common year + (&m06b_15, &m07b_20, 34, (4, 6), (1, 5), (0, 1, 5)), + ]; + + for (d0, d1, exp0, exp1, exp2, exp3) in cases { + check(d0, d1, exp0, exp1, exp2, exp3); + } +} + +#[test] +fn test_tricky_leap_months() { + let mut add_options = DateAddOptions::default(); + add_options.overflow = Some(Overflow::Constrain); + let mut until_options = DateDifferenceOptions::default(); + until_options.largest_unit = Some(DateDurationUnit::Years); + + fn hebrew_date(year: i32, month: &str, day: u8) -> Date { + Date::try_new_from_codes(None, year, MonthCode(month.parse().unwrap()), day, Hebrew) + .unwrap() + } + + // M06 + 1yr = M06 (common to leap) + let date0 = hebrew_date(5783, "M06", 20); + let duration0 = DateDuration::for_years(1); + let date1 = date0 + .try_added_with_options(duration0, add_options) + .unwrap(); + assert_eq!(date1, hebrew_date(5784, "M06", 20)); + let duration0_actual = date0.try_until_with_options(&date1, until_options).unwrap(); + assert_eq!(duration0_actual, duration0); + + // M06 - 1mo = M05L (leap to leap) + let duration1 = DateDuration::for_months(-1); + let date2 = date1 + .try_added_with_options(duration1, add_options) + .unwrap(); + assert_eq!(date2, hebrew_date(5784, "M05L", 20)); + let duration1_actual = date1.try_until_with_options(&date2, until_options).unwrap(); + assert_eq!(duration1_actual, duration1); + + // M05L + 1yr1mo = M07 (leap to common) + let duration2 = DateDuration { + years: 1, + months: 1, + ..Default::default() + }; + let date3 = date2 + .try_added_with_options(duration2, add_options) + .unwrap(); + assert_eq!(date3, hebrew_date(5785, "M07", 20)); + let duration2_actual = date2.try_until_with_options(&date3, until_options).unwrap(); + assert_eq!(duration2_actual, duration2); + + // M06 + 1yr1mo = M07 (leap to common) + let date4 = date1 + .try_added_with_options(duration2, add_options) + .unwrap(); + assert_eq!(date4, hebrew_date(5785, "M07", 20)); + let duration2_actual = date1.try_until_with_options(&date4, until_options).unwrap(); + assert_eq!(duration2_actual, duration2); +}