Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions components/calendar/src/any_calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,10 +293,18 @@ impl Calendar for AnyCalendar {
Ok(match_cal!(match self: (c) => c.from_fields(fields, options)?))
}

// This pessimises conversions between non-Gregorian calendars,
// but optimises conversions between Gregorian calendars.
const HAS_CHEAP_ISO_CONVERSION: bool = true;

fn from_iso(&self, iso: IsoDateInner) -> AnyDateInner {
match_cal!(match self: (c) => c.from_iso(iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
match_cal_and_date!(match (self, date): (c, d) => c.to_iso(d))
}

fn from_rata_die(&self, rd: calendrical_calculations::rata_die::RataDie) -> Self::DateInner {
match_cal!(match self: (c) => c.from_rata_die(rd))
}
Expand All @@ -305,10 +313,6 @@ impl Calendar for AnyCalendar {
match_cal_and_date!(match (self, date): (c, d) => c.to_rata_die(d))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
match_cal_and_date!(match (self, date): (c, d) => c.to_iso(d))
}

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
match_cal_and_date!(match (self, date): (c, d) => c.months_in_year(d))
}
Expand Down
4 changes: 4 additions & 0 deletions components/calendar/src/cal/abstract_gregorian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ impl<Y: GregorianYears> Calendar for AbstractGregorian<Y> {
calendrical_calculations::gregorian::fixed_from_gregorian(date.year, date.month, date.day)
}

const HAS_CHEAP_ISO_CONVERSION: bool = true;

fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
iso.0
}
Expand Down Expand Up @@ -259,6 +261,8 @@ macro_rules! impl_with_abstract_gregorian {
crate::cal::abstract_gregorian::AbstractGregorian($eras_expr).to_rata_die(&date.0)
}

const HAS_CHEAP_ISO_CONVERSION: bool = true;

fn from_iso(&self, iso: crate::cal::iso::IsoDateInner) -> Self::DateInner {
let $self_ident = self;
$inner_date_ty(
Expand Down
21 changes: 2 additions & 19 deletions components/calendar/src/cal/chinese.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::cal::iso::Iso;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::calendar_arithmetic::{ArithmeticDate, ToExtendedYear};
use crate::error::{
Expand Down Expand Up @@ -638,24 +638,7 @@ impl<R: Rules> Calendar for LunarChinese<R> {
date.0.year.rd_from_md(date.0.month, date.0.day)
}

fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
let rd = Iso.to_rata_die(&iso);
let y = {
let candidate = self.0.year_data(iso.0.year);

if rd >= candidate.new_year() {
candidate
} else {
self.0.year_data(iso.0.year - 1)
}
};
let (m, d) = y.md_from_rd(rd);
ChineseDateInner(ArithmeticDate::new_unchecked(y, m, d))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

// Count the number of months in a given year, specified by providing a date
// from that year
Expand Down
9 changes: 1 addition & 8 deletions components/calendar/src/cal/coptic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::error::{
Expand Down Expand Up @@ -162,13 +161,7 @@ impl Calendar for Coptic {
calendrical_calculations::coptic::fixed_from_coptic(date.0.year, date.0.month, date.0.day)
}

fn from_iso(&self, iso: IsoDateInner) -> CopticDateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
Expand Down
13 changes: 3 additions & 10 deletions components/calendar/src/cal/ethiopian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::coptic::CopticDateInner;
use crate::cal::iso::IsoDateInner;
use crate::cal::Coptic;
use crate::calendar_arithmetic::{ArithmeticDate, DateFieldsResolver};
use crate::error::{
Expand Down Expand Up @@ -133,8 +132,8 @@ impl DateFieldsResolver for Ethiopian {
impl crate::cal::scaffold::UnstableSealed for Ethiopian {}
impl Calendar for Ethiopian {
type DateInner = EthiopianDateInner;
type Year = types::EraYear;
type DifferenceError = core::convert::Infallible;
type Year = <Coptic as Calendar>::Year;
type DifferenceError = <Coptic as Calendar>::DifferenceError;

fn from_codes(
&self,
Expand Down Expand Up @@ -168,13 +167,7 @@ impl Calendar for Ethiopian {
Coptic.to_rata_die(&date.0)
}

fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
EthiopianDateInner(Coptic.from_iso(iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Coptic.to_iso(&date.0)
}
const HAS_CHEAP_ISO_CONVERSION: bool = <Coptic as Calendar>::HAS_CHEAP_ISO_CONVERSION;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Coptic.months_in_year(&date.0)
Expand Down
11 changes: 2 additions & 9 deletions components/calendar/src/cal/hebrew.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::{ArithmeticDate, DateFieldsResolver, ToExtendedYear};
use crate::error::{
DateError, DateFromFieldsError, EcmaReferenceYearError, MonthCodeError, UnknownEraError,
Expand Down Expand Up @@ -318,13 +317,7 @@ impl Calendar for Hebrew {
ny + i64::from(days_preceding) + i64::from(date.0.day) - 1
}

fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
Expand Down Expand Up @@ -502,7 +495,7 @@ mod tests {

let iso_to_hebrew = iso_date.to_calendar(Hebrew);

let hebrew_to_iso = hebrew_date.to_calendar(Iso);
let hebrew_to_iso = hebrew_date.to_iso();

assert_eq!(
hebrew_to_iso, iso_date,
Expand Down
11 changes: 2 additions & 9 deletions components/calendar/src/cal/hijri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::calendar_arithmetic::ToExtendedYear;
Expand Down Expand Up @@ -937,13 +936,7 @@ impl<R: Rules> Calendar for Hijri<R> {
date.0.year.md_to_rd(date.0.month, date.0.day)
}

fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
Expand Down Expand Up @@ -1773,7 +1766,7 @@ mod test {
for (case, f_date) in SIMULATED_CASES.iter().zip(TEST_RD.iter()) {
let date = Date::try_new_hijri_with_calendar(case.year, case.month, case.day, calendar)
.unwrap();
let iso = Date::from_rata_die(RataDie::new(*f_date), Iso);
let iso = Date::from_rata_die(RataDie::new(*f_date), crate::Iso);

assert_eq!(iso.to_calendar(calendar).inner, date.inner, "{case:?}");
}
Expand Down
39 changes: 13 additions & 26 deletions components/calendar/src/cal/indian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError};
Expand Down Expand Up @@ -120,22 +119,15 @@ impl Calendar for Indian {
ArithmeticDate::from_fields(fields, options, self).map(IndianDateInner)
}

fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
self.from_iso(Iso.from_rata_die(rd))
}

fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
Iso.to_rata_die(&self.to_iso(date))
}

// Algorithms directly implemented in icu_calendar since they're not from the book
fn from_iso(&self, iso: IsoDateInner) -> IndianDateInner {
fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
let iso_year = calendrical_calculations::gregorian::year_from_fixed(rd)
.unwrap_or_else(|e| e.saturate());
// Get day number in year (1 indexed)
let day_of_year_iso =
calendrical_calculations::gregorian::days_before_month(iso.0.year, iso.0.month)
+ iso.0.day as u16;
(rd - calendrical_calculations::gregorian::day_before_year(iso_year)) as u16;
// Convert to Śaka year
let mut year = iso.0.year - YEAR_OFFSET;
let mut year = iso_year - YEAR_OFFSET;
// This is in the previous Indian year
let day_of_year_indian = if day_of_year_iso <= DAY_OFFSET {
year -= 1;
Expand Down Expand Up @@ -169,24 +161,25 @@ impl Calendar for Indian {
}

// Algorithms directly implemented in icu_calendar since they're not from the book
fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
let day_of_year_indian = self.day_of_year(date).0; // 1-indexed
let days_in_year = self.days_in_year(date);

let mut year = date.0.year + YEAR_OFFSET;
let mut year_iso = date.0.year + YEAR_OFFSET;
// days_in_year is a valid day of the year, so we check > not >=
let day_of_year_iso = if day_of_year_indian + DAY_OFFSET > days_in_year {
year += 1;
year_iso += 1;
// calculate day of year in next year
day_of_year_indian + DAY_OFFSET - days_in_year
} else {
day_of_year_indian + DAY_OFFSET
};

let (month, day) = calendrical_calculations::gregorian::year_day(year, day_of_year_iso);
IsoDateInner(ArithmeticDate::new_unchecked(year, month, day))
calendrical_calculations::gregorian::day_before_year(year_iso) + day_of_year_iso as i64
}

const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
}
Expand Down Expand Up @@ -491,10 +484,7 @@ mod tests {
fn test_roundtrip_near_rd_zero() {
for i in -1000..=1000 {
let initial = RataDie::new(i);
let result = Date::from_rata_die(initial, Iso)
.to_calendar(Indian)
.to_iso()
.to_rata_die();
let result = Date::from_rata_die(initial, Indian).to_rata_die();
assert_eq!(
initial, result,
"Roundtrip failed for initial: {initial:?}, result: {result:?}"
Expand All @@ -507,10 +497,7 @@ mod tests {
// Epoch start: RD 28570
for i in 27570..=29570 {
let initial = RataDie::new(i);
let result = Date::from_rata_die(initial, Iso)
.to_calendar(Indian)
.to_iso()
.to_rata_die();
let result = Date::from_rata_die(initial, Indian).to_rata_die();
assert_eq!(
initial, result,
"Roundtrip failed for initial: {initial:?}, result: {result:?}"
Expand Down
9 changes: 1 addition & 8 deletions components/calendar/src/cal/julian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError};
Expand Down Expand Up @@ -160,13 +159,7 @@ impl Calendar for Julian {
calendrical_calculations::julian::fixed_from_julian(date.0.year, date.0.month, date.0.day)
}

fn from_iso(&self, iso: IsoDateInner) -> JulianDateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
Expand Down
11 changes: 2 additions & 9 deletions components/calendar/src/cal/persian.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::cal::iso::{Iso, IsoDateInner};
use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::error::{DateError, DateFromFieldsError, EcmaReferenceYearError, UnknownEraError};
Expand Down Expand Up @@ -133,13 +132,7 @@ impl Calendar for Persian {
)
}

fn from_iso(&self, iso: IsoDateInner) -> PersianDateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}

fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
const HAS_CHEAP_ISO_CONVERSION: bool = false;

fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year)
Expand Down Expand Up @@ -733,7 +726,7 @@ mod tests {
for &(p_year, leap, iso_year, iso_month, iso_day) in CALENDAR_UT_AC_IR_TEST_DATA.iter() {
let persian_date = Date::try_new_persian(p_year, 1, 1).unwrap();
assert_eq!(persian_date.is_in_leap_year(), leap);
let iso_date = persian_date.to_calendar(Iso);
let iso_date = persian_date.to_iso();
assert_eq!(iso_date.era_year().year, iso_year);
assert_eq!(iso_date.month().ordinal, iso_month);
assert_eq!(iso_date.day_of_month().0, iso_day);
Expand Down
22 changes: 17 additions & 5 deletions components/calendar/src/calendar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::cal::iso::IsoDateInner;
use crate::error::{DateError, DateFromFieldsError};
use crate::options::DateFromFieldsOptions;
use crate::options::{DateAddOptions, DateDifferenceOptions};
use crate::types;
use crate::{types, Iso};
use core::fmt;

/// A calendar implementation
Expand Down Expand Up @@ -56,11 +56,23 @@ pub trait Calendar: crate::cal::scaffold::UnstableSealed {
options: DateFromFieldsOptions,
) -> Result<Self::DateInner, DateFromFieldsError>;

/// Construct the date from an ISO date
/// Whether `from_iso`/`to_iso` is more efficient
/// than `from_rata_die`/`to_rata_die`.
const HAS_CHEAP_ISO_CONVERSION: bool;
Comment on lines +59 to +61
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Another design here would be

fn cheap_to_iso(&self, date: &Self::DateInner) -> Option<IsoDateInner>;

from_iso would retain the default impl as you have here. Then the Date code would be

match from_date.cheap_to_iso(inner) {
    Some(iso_date) => to_calendar.from_iso(iso_date),
    None => to_calendar.from_rata_die(from_date.to_rata_die())
}

All of the cheap_to_iso impls should be marked as #[inline] so that DCE works nicely and avoids the branch.

I think all the pairs work:

  • ROC to ROC: uses cheap_to_iso and from_iso. OK
  • Hebrew to Hebrew: uses to_rata_die and from_rata_die. OK
  • ROC to Hebrew: uses cheap_to_iso and from_iso, where from_iso internally uses the default impl to_rata_die and from_rata_die. OK
  • Hebrew to ROC: uses to_rata_die and from_rata_die. OK

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AnyCalendar still uses the Date<A> -> RataDie -> Date<Iso> -> RataDie -> Date<B> path, if we wanted to optimise this we'd need HAS_CHEAP_ISO_CONVERSION to be a method instead of a const.

I think the solution here is basically this.


/// Construct the date from an ISO date.
///
/// Only called if `HAS_CHEAP_ISO_CONVERSION` is set.
#[expect(clippy::wrong_self_convention)]
fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner;
/// Obtain an ISO date from this date
fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner;
fn from_iso(&self, iso: IsoDateInner) -> Self::DateInner {
self.from_rata_die(Iso.to_rata_die(&iso))
}
/// Obtain an ISO date from this date.
///
/// Only called if `HAS_CHEAP_ISO_CONVERSION` is set.
fn to_iso(&self, date: &Self::DateInner) -> IsoDateInner {
Iso.from_rata_die(self.to_rata_die(date))
}
Comment on lines +67 to +75
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: If this code isn't called unless the HAS_CHEAP_ISO_CONVERSION is set, then put a debug assert unreachable in here? Or are you worried that this might be called from external code since the Calendar trait is public?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the "Only called..." part is new, and we consider this callable-stable.

I also really dislike debug asserts


/// Construct the date from a [`RataDie`]
#[expect(clippy::wrong_self_convention)]
Expand Down
Loading
Loading