use crate::weekday::Weekday;
pub(crate) const EPOCH_OFFSET_FROM_CE: i64 = -719_162;
#[inline]
pub(crate) const fn div_floor(a: i64, b: i64) -> i64 {
let q = a / b;
let r = a % b;
if r != 0 && (r < 0) != (b < 0) {
q - 1
} else {
q
}
}
#[inline]
pub(crate) const fn is_leap_year(year: i32) -> bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
#[inline]
pub(crate) const fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 0,
}
}
#[inline]
pub(crate) const fn days_in_year(year: i32) -> u32 {
if is_leap_year(year) {
366
} else {
365
}
}
pub(crate) const fn days_from_civil(year: i32, month: u32, day: u32) -> i64 {
let y = year as i64 - if month <= 2 { 1 } else { 0 };
let era = div_floor(y, 400);
let yoe = y - era * 400; let m = month as i64;
let d = day as i64;
let mp = if m > 2 { m - 3 } else { m + 9 }; let doy = (153 * mp + 2) / 5 + d - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe - 719_468
}
pub(crate) const fn civil_from_days(z: i64) -> (i64, u32, u32) {
let zz = z + 719_468;
let era = div_floor(zz, 146_097);
let doe = zz - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let m = if mp < 10 { mp + 3 } else { mp - 9 }; let year = if m <= 2 { y + 1 } else { y };
(year, m as u32, d as u32)
}
pub(crate) const fn weekday_from_days(z: i64) -> Weekday {
let sunday0 = (z + 4).rem_euclid(7);
let monday0 = (sunday0 + 6) % 7;
Weekday::from_num_days_from_monday(monday0 as u32)
}
pub(crate) const fn iso_week1_monday(year: i32) -> i64 {
let z_jan4 = days_from_civil(year, 1, 4);
let sunday0 = (z_jan4 + 4).rem_euclid(7);
let monday0 = (sunday0 + 6) % 7; z_jan4 - monday0
}
pub(crate) const fn iso_year_week(z: i64, year: i32) -> (i32, u32) {
let week = (z - iso_week1_monday(year)).div_euclid(7) + 1;
if week < 1 {
let w1_prev = iso_week1_monday(year - 1);
return (year - 1, ((z - w1_prev) / 7 + 1) as u32);
}
let w1_next = iso_week1_monday(year + 1);
if z >= w1_next {
return (year + 1, 1);
}
(year, week as u32)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::weekday::Weekday;
#[test]
fn epoch_is_day_zero() {
assert_eq!(days_from_civil(1970, 1, 1), 0);
}
#[test]
fn proleptic_year_1_matches_epoch_offset_constant() {
assert_eq!(days_from_civil(1, 1, 1), EPOCH_OFFSET_FROM_CE);
}
#[test]
fn days_from_civil_and_civil_from_days_round_trip() {
let samples = [
(1970, 1, 1),
(1969, 12, 31),
(2000, 2, 29), (1900, 2, 28), (2023, 1, 1),
(2022, 12, 31),
(1, 1, 1),
(-1, 12, 31),
(-400, 2, 29), (9999, 12, 31),
(-9999, 1, 1),
];
for &(y, m, d) in &samples {
let z = days_from_civil(y, m, d);
let (ry, rm, rd) = civil_from_days(z);
assert_eq!((ry, rm, rd), (y as i64, m, d), "round-trip failed for {y}-{m}-{d}");
}
}
#[test]
fn is_leap_year_follows_gregorian_rule() {
assert!(is_leap_year(2000)); assert!(!is_leap_year(1900)); assert!(is_leap_year(2024)); assert!(!is_leap_year(2023));
assert!(is_leap_year(-400));
assert!(!is_leap_year(-401));
}
#[test]
fn days_in_month_matches_known_lengths() {
assert_eq!(days_in_month(2023, 1), 31);
assert_eq!(days_in_month(2023, 4), 30);
assert_eq!(days_in_month(2023, 2), 28);
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(2023, 12), 31);
}
#[test]
fn days_in_year_matches_leap_rule() {
assert_eq!(days_in_year(2023), 365);
assert_eq!(days_in_year(2024), 366);
assert_eq!(days_in_year(1900), 365);
assert_eq!(days_in_year(2000), 366);
}
#[test]
fn weekday_from_days_matches_known_anchor_days() {
assert_eq!(weekday_from_days(0), Weekday::Thu);
assert_eq!(weekday_from_days(1), Weekday::Fri); assert_eq!(weekday_from_days(-1), Weekday::Wed); assert_eq!(weekday_from_days(7), Weekday::Thu); assert_eq!(weekday_from_days(-7), Weekday::Thu); }
#[test]
fn iso_week1_monday_matches_hand_verified_years() {
assert_eq!(iso_week1_monday(2023), days_from_civil(2023, 1, 2));
assert_eq!(iso_week1_monday(2022), days_from_civil(2022, 1, 3));
}
#[test]
fn iso_year_week_first_week_of_a_typical_year() {
let z = days_from_civil(2023, 1, 4);
assert_eq!(iso_year_week(z, 2023), (2023, 1));
}
#[test]
fn iso_year_week_handles_year_boundary_belonging_to_previous_iso_year() {
let z = days_from_civil(2023, 1, 1);
assert_eq!(iso_year_week(z, 2023), (2022, 52));
}
#[test]
fn iso_year_week_handles_year_boundary_belonging_to_next_iso_year() {
let z = days_from_civil(2018, 12, 31);
assert_eq!(iso_year_week(z, 2018), (2019, 1));
}
#[test]
fn div_floor_rounds_toward_negative_infinity() {
assert_eq!(div_floor(7, 2), 3);
assert_eq!(div_floor(-7, 2), -4);
assert_eq!(div_floor(6, 2), 3);
assert_eq!(div_floor(-6, 2), -3);
assert_eq!(div_floor(0, 5), 0);
}
}