use crate::any_calendar::AnyCalendarKind;
use crate::calendar_arithmetic::{ArithmeticDate, CalendarArithmetic};
use crate::helpers::quotient;
use crate::iso::Iso;
use crate::{types, Calendar, CalendarError, Date, DateDuration, DateDurationUnit, DateTime};
use core::marker::PhantomData;
use tinystr::tinystr;
const JULIAN_EPOCH: i32 = -1;
#[derive(Copy, Clone, Debug, Hash, Default, Eq, PartialEq)]
#[allow(clippy::exhaustive_structs)] pub struct Julian;
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct JulianDateInner(pub(crate) ArithmeticDate<Julian>);
impl CalendarArithmetic for Julian {
fn month_days(year: i32, month: u8) -> u8 {
match month {
4 | 6 | 9 | 11 => 30,
2 if Self::is_leap_year(year) => 29,
2 => 28,
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
_ => 0,
}
}
fn months_for_every_year(_: i32) -> u8 {
12
}
fn is_leap_year(year: i32) -> bool {
Self::is_leap_year_const(year)
}
fn days_in_provided_year(year: i32) -> u32 {
if Self::is_leap_year(year) {
366
} else {
365
}
}
}
impl Calendar for Julian {
type DateInner = JulianDateInner;
fn date_from_codes(
&self,
era: types::Era,
year: i32,
month_code: types::MonthCode,
day: u8,
) -> Result<Self::DateInner, CalendarError> {
let year = if era.0 == tinystr!(16, "ad") {
if year <= 0 {
return Err(CalendarError::OutOfRange);
}
year
} else if era.0 == tinystr!(16, "bc") {
if year <= 0 {
return Err(CalendarError::OutOfRange);
}
1 - year
} else {
return Err(CalendarError::UnknownEra(era.0, self.debug_name()));
};
ArithmeticDate::new_from_solar(self, year, month_code, day).map(JulianDateInner)
}
fn date_from_iso(&self, iso: Date<Iso>) -> JulianDateInner {
let fixed_iso = Iso::fixed_from_iso(*iso.inner());
Self::julian_from_fixed(fixed_iso)
}
fn date_to_iso(&self, date: &Self::DateInner) -> Date<Iso> {
let fixed_julian = Julian::fixed_from_julian(date.0);
Iso::iso_from_fixed(fixed_julian)
}
fn months_in_year(&self, date: &Self::DateInner) -> u8 {
date.0.months_in_year()
}
fn days_in_year(&self, date: &Self::DateInner) -> u32 {
date.0.days_in_year()
}
fn days_in_month(&self, date: &Self::DateInner) -> u8 {
date.0.days_in_month()
}
fn day_of_week(&self, date: &Self::DateInner) -> types::IsoWeekday {
Iso.day_of_week(Julian.date_to_iso(date).inner())
}
fn offset_date(&self, date: &mut Self::DateInner, offset: DateDuration<Self>) {
date.0.offset_date(offset);
}
#[allow(clippy::field_reassign_with_default)]
fn until(
&self,
date1: &Self::DateInner,
date2: &Self::DateInner,
_calendar2: &Self,
_largest_unit: DateDurationUnit,
_smallest_unit: DateDurationUnit,
) -> DateDuration<Self> {
date1.0.until(date2.0, _largest_unit, _smallest_unit)
}
fn year(&self, date: &Self::DateInner) -> types::FormattableYear {
crate::gregorian::year_as_gregorian(date.0.year)
}
fn month(&self, date: &Self::DateInner) -> types::FormattableMonth {
date.0.solar_month()
}
fn day_of_month(&self, date: &Self::DateInner) -> types::DayOfMonth {
date.0.day_of_month()
}
fn day_of_year_info(&self, date: &Self::DateInner) -> types::DayOfYearInfo {
let prev_year = date.0.year - 1;
let next_year = date.0.year + 1;
types::DayOfYearInfo {
day_of_year: date.0.day_of_year(),
days_in_year: date.0.days_in_year(),
prev_year: crate::gregorian::year_as_gregorian(prev_year),
days_in_prev_year: Julian::days_in_year_direct(prev_year),
next_year: crate::gregorian::year_as_gregorian(next_year),
}
}
fn debug_name(&self) -> &'static str {
"Julian"
}
fn any_calendar_kind(&self) -> Option<AnyCalendarKind> {
None
}
}
impl Julian {
pub fn new() -> Self {
Self
}
#[inline(always)]
const fn is_leap_year_const(year: i32) -> bool {
year % 4 == 0
}
pub(crate) const fn fixed_from_julian(date: ArithmeticDate<Julian>) -> i32 {
let year = if date.year < 0 {
date.year + 1
} else {
date.year
};
let mut fixed: i32 = JULIAN_EPOCH - 1 + 365 * (year - 1) + quotient(year - 1, 4);
fixed += quotient(367 * (date.month as i32) - 362, 12);
fixed += if date.month <= 2 {
0
} else if Self::is_leap_year_const(date.year) {
-1
} else {
-2
};
fixed + (date.day as i32)
}
pub(crate) const fn fixed_from_julian_integers(year: i32, month: u8, day: u8) -> i32 {
Self::fixed_from_julian(ArithmeticDate {
year,
month,
day,
marker: PhantomData,
})
}
fn days_in_year_direct(year: i32) -> u32 {
if Julian::is_leap_year(year) {
366
} else {
365
}
}
fn julian_from_fixed(date: i32) -> JulianDateInner {
let approx = quotient((4 * date) + 1464, 1461);
let year = if approx <= 0 { approx - 1 } else { approx };
let prior_days = date - Self::fixed_from_julian_integers(year, 1, 1);
let correction = if date < Self::fixed_from_julian_integers(year, 3, 1) {
0
} else if Julian::is_leap_year(year) {
1
} else {
2
};
let month = quotient(12 * (prior_days + correction) + 373, 367) as u8; let day = (date - Self::fixed_from_julian_integers(year, month, 1) + 1) as u8;
#[allow(clippy::unwrap_used)] *Date::try_new_julian_date(year, month, day).unwrap().inner()
}
}
impl Date<Julian> {
pub fn try_new_julian_date(
year: i32,
month: u8,
day: u8,
) -> Result<Date<Julian>, CalendarError> {
let inner = ArithmeticDate {
year,
month,
day,
marker: PhantomData,
};
if day > 28 {
let bound = inner.days_in_month();
if day > bound {
return Err(CalendarError::OutOfRange);
}
}
Ok(Date::from_raw(JulianDateInner(inner), Julian))
}
}
impl DateTime<Julian> {
pub fn try_new_julian_datetime(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
) -> Result<DateTime<Julian>, CalendarError> {
Ok(DateTime {
date: Date::try_new_julian_date(year, month, day)?,
time: types::Time::try_new(hour, minute, second, 0)?,
})
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_day_iso_to_julian() {
let iso_date = Date::try_new_iso_date(200, 3, 1).unwrap();
let julian_date = Julian.date_from_iso(iso_date);
assert_eq!(julian_date.0.year, 200);
assert_eq!(julian_date.0.month, 3);
assert_eq!(julian_date.0.day, 1);
let iso_date = Date::try_new_iso_date(200, 2, 28).unwrap();
let julian_date = Julian.date_from_iso(iso_date);
assert_eq!(julian_date.0.year, 200);
assert_eq!(julian_date.0.month, 2);
assert_eq!(julian_date.0.day, 29);
let iso_date = Date::try_new_iso_date(400, 3, 1).unwrap();
let julian_date = Julian.date_from_iso(iso_date);
assert_eq!(julian_date.0.year, 400);
assert_eq!(julian_date.0.month, 2);
assert_eq!(julian_date.0.day, 29);
let iso_date = Date::try_new_iso_date(2022, 1, 1).unwrap();
let julian_date = Julian.date_from_iso(iso_date);
assert_eq!(julian_date.0.year, 2021);
assert_eq!(julian_date.0.month, 12);
assert_eq!(julian_date.0.day, 19);
}
#[test]
fn test_day_julian_to_iso() {
let julian_date = Date::try_new_julian_date(200, 3, 1).unwrap();
let iso_date = Julian.date_to_iso(julian_date.inner());
let iso_expected_date = Date::try_new_iso_date(200, 3, 1).unwrap();
assert_eq!(iso_date, iso_expected_date);
let julian_date = Date::try_new_julian_date(200, 2, 29).unwrap();
let iso_date = Julian.date_to_iso(julian_date.inner());
let iso_expected_date = Date::try_new_iso_date(200, 2, 28).unwrap();
assert_eq!(iso_date, iso_expected_date);
let julian_date = Date::try_new_julian_date(400, 2, 29).unwrap();
let iso_date = Julian.date_to_iso(julian_date.inner());
let iso_expected_date = Date::try_new_iso_date(400, 3, 1).unwrap();
assert_eq!(iso_date, iso_expected_date);
let julian_date = Date::try_new_julian_date(2021, 12, 19).unwrap();
let iso_date = Julian.date_to_iso(julian_date.inner());
let iso_expected_date = Date::try_new_iso_date(2022, 1, 1).unwrap();
assert_eq!(iso_date, iso_expected_date);
let julian_date = Date::try_new_julian_date(2022, 2, 16).unwrap();
let iso_date = Julian.date_to_iso(julian_date.inner());
let iso_expected_date = Date::try_new_iso_date(2022, 3, 1).unwrap();
assert_eq!(iso_date, iso_expected_date);
}
#[test]
fn test_roundtrip_negative() {
let iso_date = Date::try_new_iso_date(-1000, 3, 3).unwrap();
let julian = iso_date.to_calendar(Julian::new());
let recovered_iso = julian.to_iso();
assert_eq!(iso_date, recovered_iso);
}
}