#![allow(missing_docs)]
#[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: u8) -> u8 {
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, }
}
pub(crate) const fn civil_from_days(z: i64) -> (i32, u8, u8) {
let z = z + 719_468;
let era: i64 = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe as i64 + 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 y = if m <= 2 { y + 1 } else { y };
(y as i32, m as u8, d as u8)
}
pub(crate) const fn days_from_civil(y: i32, m: u8, d: u8) -> i64 {
let y: i64 = if m <= 2 { y as i64 - 1 } else { y as i64 };
let era: i64 = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u64; let m_idx: u64 = if m > 2 { m as u64 - 3 } else { m as u64 + 9 }; let doy = (153 * m_idx + 2) / 5 + d as u64 - 1; let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; era * 146_097 + doe as i64 - 719_468
}
#[inline]
pub(crate) const fn weekday_from_days(z: i64) -> u8 {
let d = z.rem_euclid(7) as u8;
(d + 3) % 7 + 1
}
pub(crate) const NANOS_PER_SEC: i64 = 1_000_000_000;
pub(crate) const NANOS_PER_DAY: i64 = 86_400 * NANOS_PER_SEC;
pub(crate) fn local_fields(ts_nanos: i64, offset_secs: i32) -> (i32, u8, u8, u8, u8, u8, u32) {
let local = ts_nanos + offset_secs as i64 * NANOS_PER_SEC;
let days = local.div_euclid(NANOS_PER_DAY);
let nanos_in_day = local.rem_euclid(NANOS_PER_DAY);
let (y, m, d) = civil_from_days(days);
let secs_in_day = (nanos_in_day / NANOS_PER_SEC) as u32;
let h = (secs_in_day / 3_600) as u8;
let min = ((secs_in_day % 3_600) / 60) as u8;
let s = (secs_in_day % 60) as u8;
let ns = (nanos_in_day % NANOS_PER_SEC) as u32;
(y, m, d, h, min, s, ns)
}
#[allow(clippy::too_many_arguments)]
pub(crate) const fn ts_from_fields(
year: i32,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
nanosecond: u32,
offset_secs: i32,
) -> i64 {
let days = days_from_civil(year, month, day);
let time_nanos = hour as i64 * 3_600 * NANOS_PER_SEC
+ minute as i64 * 60 * NANOS_PER_SEC
+ second as i64 * NANOS_PER_SEC
+ nanosecond as i64;
days * NANOS_PER_DAY + time_nanos - offset_secs as i64 * NANOS_PER_SEC
}
pub(crate) const fn add_months_ym(year: i32, month: u8, months: i32) -> (i32, u8) {
let total_months = (year as i64) * 12 + (month as i64 - 1) + months as i64;
let new_year = total_months.div_euclid(12) as i32;
let new_month = (total_months.rem_euclid(12) + 1) as u8;
(new_year, new_month)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_epoch() {
assert_eq!(civil_from_days(0), (1970, 1, 1));
assert_eq!(days_from_civil(1970, 1, 1), 0);
}
#[test]
fn round_trip_various() {
let cases: &[(i32, u8, u8)] = &[
(2024, 3, 15),
(2000, 1, 1),
(1999, 12, 31),
(2016, 2, 29), (1969, 12, 31),
(1, 1, 1),
];
for &(y, m, d) in cases {
let days = days_from_civil(y, m, d);
assert_eq!(civil_from_days(days), (y, m, d), "{y}-{m:02}-{d:02}");
}
}
#[test]
fn leap_year_checks() {
assert!(is_leap_year(2000));
assert!(is_leap_year(2024));
assert!(!is_leap_year(1900));
assert!(!is_leap_year(2023));
}
#[test]
fn days_in_month_checks() {
assert_eq!(days_in_month(2024, 2), 29);
assert_eq!(days_in_month(2023, 2), 28);
assert_eq!(days_in_month(2024, 1), 31);
assert_eq!(days_in_month(2024, 4), 30);
}
#[test]
fn weekday_epoch() {
assert_eq!(weekday_from_days(0), 4);
}
#[test]
fn add_months_wrap() {
assert_eq!(add_months_ym(2023, 11, 3), (2024, 2));
assert_eq!(add_months_ym(2024, 1, -1), (2023, 12));
}
}