use crate::calendar_arithmetic::ArithmeticDate;
use crate::calendar_arithmetic::DateFieldsResolver;
use crate::error::{
DateAddError, DateFromFieldsError, DateNewError, EcmaReferenceYearError, UnknownEraError,
};
use crate::options::DateFromFieldsOptions;
use crate::options::{DateAddOptions, DateDifferenceOptions};
use crate::types::DateFields;
use crate::{types, Calendar, Date, RangeError};
use calendrical_calculations::rata_die::RataDie;
use tinystr::tinystr;
#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq, PartialOrd, Ord)]
#[allow(clippy::exhaustive_structs)] pub struct Julian;
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
impl DateFieldsResolver for Julian {
type YearInfo = i32;
fn days_in_provided_month(year: i32, month: u8) -> u8 {
if month == 2 {
28 + calendrical_calculations::julian::is_leap_year(year) as u8
} else {
30 | month ^ (month >> 3)
}
}
#[inline]
fn extended_year_from_era_year_unchecked(
&self,
era: &[u8],
era_year: i32,
) -> Result<i32, UnknownEraError> {
match era {
b"ad" | b"ce" => Ok(era_year),
b"bc" | b"bce" => Ok(1 - era_year),
_ => Err(UnknownEraError),
}
}
#[inline]
fn year_info_from_extended(&self, extended_year: i32) -> Self::YearInfo {
extended_year
}
#[inline]
fn reference_year_from_month_day(
&self,
month: types::Month,
day: u8,
) -> Result<Self::YearInfo, EcmaReferenceYearError> {
let (ordinal_month, false) = (month.number(), month.is_leap()) else {
return Err(EcmaReferenceYearError::MonthNotInCalendar);
};
let julian_year = if ordinal_month < 12 || (ordinal_month == 12 && day <= 18) {
1972
} else {
1971
};
Ok(julian_year)
}
fn to_rata_die_inner(year: Self::YearInfo, month: u8, day: u8) -> RataDie {
calendrical_calculations::julian::fixed_from_julian(year, month, day)
}
}
impl crate::cal::scaffold::UnstableSealed for Julian {}
impl Calendar for Julian {
type DateInner = JulianDateInner;
type Year = types::EraYear;
type DateCompatibilityError = core::convert::Infallible;
fn new_date(
&self,
year: types::YearInput,
month: types::Month,
day: u8,
) -> Result<Self::DateInner, DateNewError> {
ArithmeticDate::from_input_year_month_code_day(year, month, day, self).map(JulianDateInner)
}
fn from_fields(
&self,
fields: DateFields,
options: DateFromFieldsOptions,
) -> Result<Self::DateInner, DateFromFieldsError> {
ArithmeticDate::from_fields(fields, options, self).map(JulianDateInner)
}
fn from_rata_die(&self, rd: RataDie) -> Self::DateInner {
let (year, month, day) =
calendrical_calculations::julian::julian_from_fixed(rd).unwrap_or((1, 1, 1));
JulianDateInner(ArithmeticDate::new_unchecked(year, month, day))
}
fn to_rata_die(&self, date: &Self::DateInner) -> RataDie {
date.0.to_rata_die()
}
fn has_cheap_iso_conversion(&self) -> bool {
false
}
fn months_in_year(&self, date: &Self::DateInner) -> u8 {
Self::months_in_provided_year(date.0.year())
}
fn days_in_year(&self, date: &Self::DateInner) -> u16 {
365 + calendrical_calculations::julian::is_leap_year(date.0.year()) as u16
}
fn days_in_month(&self, date: &Self::DateInner) -> u8 {
Self::days_in_provided_month(date.0.year(), date.0.month())
}
fn add(
&self,
date: &Self::DateInner,
duration: types::DateDuration,
options: DateAddOptions,
) -> Result<Self::DateInner, DateAddError> {
date.0.added(duration, self, options).map(JulianDateInner)
}
fn until(
&self,
date1: &Self::DateInner,
date2: &Self::DateInner,
options: DateDifferenceOptions,
) -> types::DateDuration {
date1.0.until(&date2.0, self, options)
}
fn check_date_compatibility(&self, &Self: &Self) -> Result<(), Self::DateCompatibilityError> {
Ok(())
}
fn year_info(&self, date: &Self::DateInner) -> Self::Year {
let extended_year = date.0.year();
if extended_year > 0 {
types::EraYear {
era: tinystr!(16, "ce"),
era_index: Some(1),
year: extended_year,
extended_year,
ambiguity: types::YearAmbiguity::CenturyRequired,
}
} else {
types::EraYear {
era: tinystr!(16, "bce"),
era_index: Some(0),
year: 1 - extended_year,
extended_year,
ambiguity: types::YearAmbiguity::EraAndCenturyRequired,
}
}
}
fn is_in_leap_year(&self, date: &Self::DateInner) -> bool {
calendrical_calculations::julian::is_leap_year(date.0.year())
}
fn month(&self, date: &Self::DateInner) -> types::MonthInfo {
types::MonthInfo::new(self, date.0)
}
fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
types::DayOfMonth(date.0.day())
}
fn day_of_year(&self, date: &Self::DateInner) -> types::DayOfYear {
types::DayOfYear(
calendrical_calculations::julian::days_before_month(date.0.year(), date.0.month())
+ date.0.day() as u16,
)
}
fn debug_name(&self) -> &'static str {
"Julian"
}
fn calendar_algorithm(&self) -> Option<crate::preferences::CalendarAlgorithm> {
None
}
}
impl Julian {
pub fn new() -> Self {
Self
}
pub fn easter(year: i32) -> Date<Self> {
Date::from_rata_die(calendrical_calculations::julian::easter(year), Self)
}
}
impl Date<Julian> {
pub fn try_new_julian(year: i32, month: u8, day: u8) -> Result<Date<Julian>, RangeError> {
ArithmeticDate::from_year_month_day(year, month, day, &Julian)
.map(JulianDateInner)
.map(|inner| Date::from_raw(inner, Julian))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_julian() {
let cases = [
TestCase {
rd: Date::try_new_iso(200, 3, 1).unwrap().to_rata_die(),
era: "ce",
year: 200,
month: 3,
day: 1,
},
TestCase {
rd: Date::try_new_iso(200, 2, 28).unwrap().to_rata_die(),
era: "ce",
year: 200,
month: 2,
day: 29,
},
TestCase {
rd: Date::try_new_iso(400, 3, 1).unwrap().to_rata_die(),
era: "ce",
year: 400,
month: 2,
day: 29,
},
TestCase {
rd: Date::try_new_iso(2022, 1, 1).unwrap().to_rata_die(),
era: "ce",
year: 2021,
month: 12,
day: 19,
},
TestCase {
era: "ce",
year: 2022,
month: 2,
day: 16,
rd: Date::try_new_iso(2022, 3, 1).unwrap().to_rata_die(),
},
];
for case in cases {
check_case(case)
}
}
#[test]
fn test_roundtrip_negative() {
let rd = Date::try_new_iso(-1000, 3, 3).unwrap().to_rata_die();
let julian = Date::from_rata_die(rd, Julian::new());
let recovered_rd = julian.to_rata_die();
assert_eq!(rd, recovered_rd);
}
#[derive(Debug)]
struct TestCase {
rd: RataDie,
year: i32,
era: &'static str,
month: u8,
day: u8,
}
fn check_case(case: TestCase) {
let date = Date::from_rata_die(case.rd, Julian);
assert_eq!(date.to_rata_die(), case.rd, "{case:?}");
assert_eq!(date.era_year().year, case.year, "{case:?}");
assert_eq!(date.era_year().era, case.era, "{case:?}");
assert_eq!(date.month().ordinal, case.month, "{case:?}");
assert_eq!(date.day_of_month().0, case.day, "{case:?}");
assert_eq!(
Date::try_new_julian(
date.era_year().extended_year,
date.month().ordinal,
date.day_of_month().0
),
Ok(date),
"{case:?}"
);
}
#[test]
fn test_julian_near_era_change() {
let cases = [
TestCase {
rd: RataDie::new(1),
year: 1,
era: "ce",
month: 1,
day: 3,
},
TestCase {
rd: RataDie::new(0),
year: 1,
era: "ce",
month: 1,
day: 2,
},
TestCase {
rd: RataDie::new(-1),
year: 1,
era: "ce",
month: 1,
day: 1,
},
TestCase {
rd: RataDie::new(-2),
year: 1,
era: "bce",
month: 12,
day: 31,
},
TestCase {
rd: RataDie::new(-3),
year: 1,
era: "bce",
month: 12,
day: 30,
},
TestCase {
rd: RataDie::new(-367),
year: 1,
era: "bce",
month: 1,
day: 1,
},
TestCase {
rd: RataDie::new(-368),
year: 2,
era: "bce",
month: 12,
day: 31,
},
TestCase {
rd: RataDie::new(-1462),
year: 4,
era: "bce",
month: 1,
day: 1,
},
TestCase {
rd: RataDie::new(-1463),
year: 5,
era: "bce",
month: 12,
day: 31,
},
];
for case in cases {
check_case(case)
}
}
#[test]
fn test_julian_rd_date_conversion() {
for i in -10000..=10000 {
let rd = RataDie::new(i);
let julian = Date::from_rata_die(rd, Julian);
let new_rd = julian.to_rata_die();
assert_eq!(rd, new_rd);
}
}
#[test]
fn test_julian_directionality() {
for i in -100..=100 {
for j in -100..=100 {
let julian_i = Date::from_rata_die(RataDie::new(i), Julian);
let julian_j = Date::from_rata_die(RataDie::new(j), Julian);
assert_eq!(
i.cmp(&j),
julian_i.inner().0.cmp(&julian_j.inner().0),
"Julian directionality inconsistent with directionality for i: {i}, j: {j}"
);
}
}
}
#[test]
fn test_julian_leap_years() {
Date::try_new_julian(4, 2, 29).unwrap();
Date::try_new_julian(0, 2, 29).unwrap();
Date::try_new_julian(-4, 2, 29).unwrap();
Date::try_new_julian(2020, 2, 29).unwrap();
}
}