const DAYS_PER_ERA: i64 = 146_097;
const EPOCH_ERA_OFFSET: i64 = 719_468;
const EPOCH_WHOLE_ERAS: i64 = 4;
const EPOCH_ERA_REMAINDER: i64 = 135_080;
fn divide(numerator: i64, denominator: i64) -> i64 {
numerator.checked_div(denominator).unwrap_or(0)
}
#[must_use]
pub fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
#[must_use]
pub fn try_days_in_month(year: i64, month: u32) -> Option<u32> {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => Some(31),
4 | 6 | 9 | 11 => Some(30),
2 if is_leap(year) => Some(29),
2 => Some(28),
_ => None,
}
}
#[must_use]
pub fn days_in_month(year: i64, month: u32) -> u32 {
try_days_in_month(year, month).unwrap_or(0)
}
#[must_use]
pub fn days_from_civil(year: i64, month: u32, day: u32) -> i64 {
let year = i128::from(year);
let month = i128::from(month);
let day = i128::from(day);
let adjusted_year = if month <= 2 {
year.saturating_sub(1)
} else {
year
};
let era = adjusted_year.div_euclid(400);
let year_of_era = adjusted_year.saturating_sub(era.saturating_mul(400));
let month_prime = if month > 2 {
month.saturating_sub(3)
} else {
month.saturating_add(9)
};
let day_of_year = month_prime
.saturating_mul(153)
.saturating_add(2)
.checked_div(5)
.unwrap_or(0)
.saturating_add(day)
.saturating_sub(1);
let day_of_era = year_of_era
.saturating_mul(365)
.saturating_add(year_of_era.checked_div(4).unwrap_or(0))
.saturating_sub(year_of_era.checked_div(100).unwrap_or(0))
.saturating_add(day_of_year);
let days = era
.saturating_mul(i128::from(DAYS_PER_ERA))
.saturating_add(day_of_era)
.saturating_sub(i128::from(EPOCH_ERA_OFFSET));
i64::try_from(days).unwrap_or(if days.is_negative() {
i64::MIN
} else {
i64::MAX
})
}
fn shifted_to_era(days_since_epoch: i64) -> (i64, i64) {
let base_era = days_since_epoch.div_euclid(DAYS_PER_ERA);
let within_era = days_since_epoch.rem_euclid(DAYS_PER_ERA);
let rebased = within_era.saturating_add(EPOCH_ERA_REMAINDER);
if rebased < DAYS_PER_ERA {
(base_era.saturating_add(EPOCH_WHOLE_ERAS), rebased)
} else {
(
base_era.saturating_add(EPOCH_WHOLE_ERAS).saturating_add(1),
rebased.saturating_sub(DAYS_PER_ERA),
)
}
}
fn era_to_year(day_of_era: i64, era: i64) -> (i64, i64) {
let year_of_era = divide(
day_of_era
.saturating_sub(divide(day_of_era, 1_460))
.saturating_add(divide(day_of_era, 36_524))
.saturating_sub(divide(day_of_era, 146_096)),
365,
);
let computed_year = year_of_era.saturating_add(era.saturating_mul(400));
let day_of_year = day_of_era.saturating_sub(
year_of_era
.saturating_mul(365)
.saturating_add(divide(year_of_era, 4))
.saturating_sub(divide(year_of_era, 100)),
);
(computed_year, day_of_year)
}
fn day_of_year_to_month_day(day_of_year: i64, computed_year: i64) -> (i64, i64, i64) {
let month_prime = divide(day_of_year.saturating_mul(5).saturating_add(2), 153);
let computed_day = day_of_year
.saturating_sub(divide(month_prime.saturating_mul(153).saturating_add(2), 5))
.saturating_add(1);
let computed_month = if month_prime < 10 {
month_prime.saturating_add(3)
} else {
month_prime.saturating_sub(9)
};
let final_year = if computed_month <= 2 {
computed_year.saturating_add(1)
} else {
computed_year
};
(final_year, computed_month, computed_day)
}
#[must_use]
pub fn civil_from_days(days: i64) -> (i64, u32, u32) {
let (era, day_of_era) = shifted_to_era(days);
let (computed_year, day_of_year) = era_to_year(day_of_era, era);
let (year, month, day) = day_of_year_to_month_day(day_of_year, computed_year);
(
year,
u32::try_from(month).unwrap_or(1),
u32::try_from(day).unwrap_or(1),
)
}