use std::time::{SystemTime, UNIX_EPOCH};
use super::policy::PacPolicy;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Civil {
pub(crate) year: i64,
pub(crate) month: i64,
pub(crate) day: i64,
pub(crate) hour: i64,
pub(crate) minute: i64,
pub(crate) second: i64,
pub(crate) weekday: i64,
}
pub(crate) fn civil_now(policy: &PacPolicy, gmt: bool) -> Civil {
let base = policy.now().unwrap_or_else(SystemTime::now);
let secs = match base.duration_since(UNIX_EPOCH) {
Ok(delta) => i64::try_from(delta.as_secs()).unwrap_or(i64::MAX),
Err(err) => {
let magnitude = err.duration();
let secs = magnitude
.as_secs()
.saturating_add(u64::from(magnitude.subsec_nanos() != 0));
-i64::try_from(secs).unwrap_or(i64::MAX)
}
};
let secs = if gmt {
secs
} else {
secs.saturating_add(i64::from(policy.local_utc_offset()))
};
civil_from_unix(secs)
}
pub(crate) fn civil_from_unix(secs: i64) -> Civil {
let days = secs.div_euclid(86_400);
let rem = secs.rem_euclid(86_400);
let (year, month, day) = civil_from_days(days);
Civil {
year,
month,
day,
hour: rem / 3600,
minute: (rem / 60) % 60,
second: rem % 60,
weekday: (days + 4).rem_euclid(7),
}
}
fn civil_from_days(z: i64) -> (i64, i64, i64) {
let z = z + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - 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 }; (if m <= 2 { y + 1 } else { y }, m, d)
}
pub(crate) fn in_cyclic_range(value: i64, lo: i64, hi: i64) -> bool {
if lo <= hi {
lo <= value && value <= hi
} else {
value >= lo || value <= hi
}
}
pub(crate) fn weekday_index(name: &str) -> Option<i64> {
const DAYS: [&str; 7] = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
let name = name.trim().to_ascii_uppercase();
DAYS.iter()
.position(|day| *day == name)
.map(|index| index as i64)
}
pub(crate) fn month_index(name: &str) -> Option<i64> {
const MONTHS: [&str; 12] = [
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
];
let name = name.trim().to_ascii_uppercase();
MONTHS
.iter()
.position(|month| *month == name)
.map(|index| index as i64 + 1)
}
pub(crate) fn split_gmt(args: &[String]) -> (&[String], bool) {
match args.last() {
Some(last) if last.trim().eq_ignore_ascii_case("GMT") => (&args[..args.len() - 1], true),
_ => (args, false),
}
}
pub(crate) fn number(text: &str) -> Option<i64> {
let value: f64 = text.trim().parse().ok()?;
if value.is_finite() {
Some(value as i64)
} else {
None
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn epoch_is_a_thursday() {
let civil = civil_from_unix(0);
assert_eq!((civil.year, civil.month, civil.day), (1970, 1, 1));
assert_eq!(civil.weekday, 4);
assert_eq!((civil.hour, civil.minute, civil.second), (0, 0, 0));
}
#[test]
fn known_instants_round_trip() {
let civil = civil_from_unix(1_709_214_307);
assert_eq!((civil.year, civil.month, civil.day), (2024, 2, 29));
assert_eq!((civil.hour, civil.minute, civil.second), (13, 45, 7));
assert_eq!(civil.weekday, 4);
let civil = civil_from_unix(951_868_800);
assert_eq!((civil.year, civil.month, civil.day), (2000, 3, 1));
assert_eq!(civil.weekday, 3);
}
#[test]
fn the_two_era_corrections_are_each_reached_by_one_date() {
let civil = civil_from_unix(951_782_400);
assert_eq!((civil.year, civil.month, civil.day), (2000, 2, 29));
let civil = civil_from_unix(-62_167_219_200);
assert_eq!((civil.year, civil.month, civil.day), (0, 1, 1));
}
#[test]
fn a_pre_epoch_instant_with_a_fraction_floors_like_every_other_one() {
let policy = PacPolicy::default().with_now(UNIX_EPOCH - Duration::from_millis(500));
let civil = civil_now(&policy, true);
assert_eq!(
(civil.year, civil.month, civil.day),
(1969, 12, 31),
"1969-12-31T23:59:59.5Z must not round up into 1970"
);
assert_eq!((civil.hour, civil.minute, civil.second), (23, 59, 59));
assert_eq!(civil.weekday, 3);
}
#[test]
fn instants_before_the_epoch_floor_correctly() {
let civil = civil_from_unix(-1);
assert_eq!((civil.year, civil.month, civil.day), (1969, 12, 31));
assert_eq!((civil.hour, civil.minute, civil.second), (23, 59, 59));
assert_eq!(civil.weekday, 3);
let civil = civil_from_unix(-31_536_000);
assert_eq!((civil.year, civil.month, civil.day), (1969, 1, 1));
assert_eq!(civil.weekday, 3);
}
#[test]
fn cyclic_ranges_wrap() {
assert!(in_cyclic_range(3, 1, 5));
assert!(!in_cyclic_range(6, 1, 5));
assert!(in_cyclic_range(6, 5, 1));
assert!(in_cyclic_range(0, 5, 1));
assert!(!in_cyclic_range(3, 5, 1));
}
#[test]
fn name_tables() {
for (index, name) in ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"]
.into_iter()
.enumerate()
{
assert_eq!(weekday_index(name), Some(index as i64), "{name}");
}
for (index, name) in [
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
]
.into_iter()
.enumerate()
{
assert_eq!(month_index(name), Some(index as i64 + 1), "{name}");
}
assert_eq!(weekday_index("sun"), Some(0));
assert_eq!(month_index("jan"), Some(1));
assert_eq!(weekday_index("Someday"), None);
assert_eq!(month_index("Smarch"), None);
}
#[test]
fn the_gmt_peel_and_the_name_tables_forgive_padding_and_case() {
for spelling in ["GMT", "gmt", " GMT", "Gmt\t"] {
let args = [String::from("MON"), String::from(spelling)];
let (rest, gmt) = split_gmt(&args);
assert!(gmt, "{spelling}");
assert_eq!(rest, [String::from("MON")], "{spelling}");
}
let args = [String::from("FRI"), String::from("MON")];
assert_eq!(split_gmt(&args), (&args[..], false));
assert_eq!(weekday_index(" sun\t"), Some(0));
assert_eq!(month_index("\ndec "), Some(12));
}
#[test]
fn a_non_finite_argument_is_not_a_bound() {
assert_eq!(number("NaN"), None);
assert_eq!(number("Infinity"), None);
assert_eq!(number("-Infinity"), None);
assert_eq!(number(" 12 "), Some(12));
assert_eq!(number("13.9"), Some(13));
assert_eq!(number("MON"), None);
}
}