pub const SECONDS_PER_DAY: i64 = 86_400;
pub const MILLIS_PER_DAY: i64 = 86_400_000;
pub const EPOCH_1900_TO_1970_SECONDS: i64 = 2_208_988_800;
pub const EPOCH_1900_TO_1970_MILLIS: i64 = EPOCH_1900_TO_1970_SECONDS * 1000;
pub const LILIAN_ZERO: i64 = days_from_civil(1582, 10, 14);
pub const LAST_LILIAN: i64 = days_from_civil(9999, 12, 31) - LILIAN_ZERO;
pub const fn days_from_civil(year: i64, month: i64, day: i64) -> i64 {
let (y, m) = if month <= 2 { (year - 1, month + 9) } else { (year, month - 3) };
let era = y.div_euclid(400);
let yoe = y.rem_euclid(400);
let doe = yoe * 365 + yoe / 4 - yoe / 100 + (153 * m + 2) / 5 + day - 1;
era * 146_097 + doe - 719_468
}
pub fn is_leap(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
pub fn days_in_month(year: i64, month: u32) -> u32 {
match month {
2 if is_leap(year) => 29,
2 => 28,
4 | 6 | 9 | 11 => 30,
_ => 31,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Civil {
pub year: i64,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: u32,
pub day_of_year: u32,
pub weekday: u32,
}
impl Civil {
pub fn cics_weekday(&self) -> i64 {
i64::from(self.weekday % 7)
}
}
pub fn civil(seconds: i64) -> Civil {
let days = seconds.div_euclid(SECONDS_PER_DAY);
let secs = seconds.rem_euclid(SECONDS_PER_DAY) as u32;
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let doy_march = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy_march + 2) / 153;
let day = (doy_march - (153 * mp + 2) / 5 + 1) as u32;
let month = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
let year = yoe + era * 400 + i64::from(month <= 2);
let before = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month as usize - 1] + u32::from(is_leap(year) && month > 2);
Civil {
year,
month,
day,
hour: secs / 3600,
minute: secs / 60 % 60,
second: secs % 60,
day_of_year: before + day,
weekday: ((days + 3).rem_euclid(7) + 1) as u32,
}
}
pub fn lilian(year: i64, month: u32, day: u32) -> i64 {
days_from_civil(year, i64::from(month), i64::from(day)) - LILIAN_ZERO
}
pub fn weekday(lilian: i64) -> u32 {
((lilian + 4).rem_euclid(7) + 1) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn civil_dates() {
let epoch = civil(0);
assert_eq!((epoch.year, epoch.month, epoch.day, epoch.day_of_year, epoch.weekday), (1970, 1, 1, 1, 4));
let c = civil(1_790_510_400);
assert_eq!((c.year, c.month, c.day, c.day_of_year, c.weekday), (2026, 9, 27, 270, 7));
assert_eq!((c.hour, c.minute, c.second), (12, 0, 0));
assert_eq!(civil(951_782_400).day_of_year, 60);
}
#[test]
fn days_and_civil_round_trip() {
for (y, m, d) in [(1582, 10, 15), (1900, 3, 1), (1970, 1, 1), (2000, 2, 29), (9999, 12, 31)] {
let days = days_from_civil(y, i64::from(m), i64::from(d));
let c = civil(days * SECONDS_PER_DAY);
assert_eq!((c.year, c.month, c.day), (y, m, d));
}
assert_eq!(days_from_civil(1970, 1, 1), 0);
}
#[test]
fn leap_years() {
assert!(!is_leap(1900));
assert!(is_leap(2000));
assert!(is_leap(2024));
assert_eq!((days_in_month(1900, 2), days_in_month(2024, 2), days_in_month(2023, 4)), (28, 29, 30));
}
#[test]
fn lilian_day_one_is_15_october_1582() {
assert_eq!(lilian(1582, 10, 15), 1);
assert_eq!(LAST_LILIAN, lilian(9999, 12, 31));
}
#[test]
fn weekday_conventions() {
assert_eq!(civil(1_790_510_400).cics_weekday(), 0);
assert_eq!(civil(0).cics_weekday(), 4);
assert_eq!(weekday(1), 6);
}
#[test]
fn epoch_offset_is_seventy_years() {
assert_eq!(EPOCH_1900_TO_1970_SECONDS, -days_from_civil(1900, 1, 1) * SECONDS_PER_DAY);
}
}