const MAC_EPOCH_TO_UNIX: i64 = 2_082_844_800;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MacTimestamp(pub u32);
impl MacTimestamp {
pub fn to_unix(self) -> i64 {
self.0 as i64 - MAC_EPOCH_TO_UNIX
}
pub fn from_unix(unix: i64) -> Option<Self> {
let mac = unix.checked_add(MAC_EPOCH_TO_UNIX)?;
if (0..=u32::MAX as i64).contains(&mac) {
Some(MacTimestamp(mac as u32))
} else {
None
}
}
pub fn now() -> Self {
use std::time::{SystemTime, UNIX_EPOCH};
let unix = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs().min(i64::MAX as u64) as i64,
Err(e) => -(e.duration().as_secs().min(i64::MAX as u64) as i64),
};
let mac = unix
.saturating_add(MAC_EPOCH_TO_UNIX)
.clamp(0, u32::MAX as i64);
MacTimestamp(mac as u32)
}
pub fn to_ymd_hms(self) -> (i32, u32, u32, u32, u32, u32) {
let unix = self.to_unix();
let days = unix.div_euclid(86_400);
let secs_of_day = unix.rem_euclid(86_400);
let z = days + 719_468; let era = z.div_euclid(146_097); let doe = z.rem_euclid(146_097); let yoe = (doe - doe / 1_460 + 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 = mp + if mp < 10 { 3 } else { -9 }; let y = y + i64::from(m <= 2);
(
y as i32,
m as u32,
d as u32,
(secs_of_day / 3_600) as u32,
(secs_of_day / 60 % 60) as u32,
(secs_of_day % 60) as u32,
)
}
}
impl std::fmt::Display for MacTimestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (y, mo, d, h, mi, s) = self.to_ymd_hms();
write!(f, "{y:04}-{mo:02}-{d:02} {h:02}:{mi:02}:{s:02}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mac_epoch() {
assert_eq!(MacTimestamp(0).to_ymd_hms(), (1904, 1, 1, 0, 0, 0));
assert_eq!(MacTimestamp(0).to_unix(), -MAC_EPOCH_TO_UNIX);
}
#[test]
fn unix_epoch() {
let t = MacTimestamp(2_082_844_800);
assert_eq!(t.to_ymd_hms(), (1970, 1, 1, 0, 0, 0));
assert_eq!(t.to_unix(), 0);
}
#[test]
fn modern_value() {
let t = MacTimestamp(3_660_681_600);
assert_eq!(t.to_unix(), 1_577_836_800);
assert_eq!(t.to_ymd_hms(), (2020, 1, 1, 0, 0, 0));
}
#[test]
fn time_of_day_and_leap_days() {
let t = MacTimestamp::from_unix(443_795_696).unwrap();
assert_eq!(t.to_ymd_hms(), (1984, 1, 24, 12, 34, 56));
assert_eq!(
MacTimestamp::from_unix(951_782_400).unwrap().to_ymd_hms(),
(2000, 2, 29, 0, 0, 0)
);
assert_eq!(
MacTimestamp::from_unix(1_609_459_199).unwrap().to_ymd_hms(),
(2020, 12, 31, 23, 59, 59)
);
}
#[test]
fn from_unix_round_trip() {
for &u in &[
-MAC_EPOCH_TO_UNIX,
-1,
0,
1,
1_577_836_800,
u32::MAX as i64 - MAC_EPOCH_TO_UNIX,
] {
let t = MacTimestamp::from_unix(u).expect("in range");
assert_eq!(t.to_unix(), u);
assert_eq!(MacTimestamp::from_unix(t.to_unix()), Some(t));
}
}
#[test]
fn from_unix_out_of_range() {
assert_eq!(MacTimestamp::from_unix(-MAC_EPOCH_TO_UNIX - 1), None);
assert_eq!(
MacTimestamp::from_unix(u32::MAX as i64 - MAC_EPOCH_TO_UNIX + 1),
None
);
assert_eq!(MacTimestamp::from_unix(i64::MAX), None);
assert_eq!(MacTimestamp::from_unix(i64::MIN), None);
}
#[test]
fn display_format() {
assert_eq!(MacTimestamp(0).to_string(), "1904-01-01 00:00:00");
assert_eq!(
MacTimestamp(2_082_844_800).to_string(),
"1970-01-01 00:00:00"
);
assert_eq!(
MacTimestamp::from_unix(443_795_696).unwrap().to_string(),
"1984-01-24 12:34:56"
);
assert_eq!(MacTimestamp(u32::MAX).to_string(), "2040-02-06 06:28:15");
}
#[test]
fn now_is_plausible() {
let now = MacTimestamp::now();
assert!(now.to_unix() > 1_704_067_200, "now() = {now}");
assert!(now.0 < u32::MAX);
}
#[test]
fn ordering_is_chronological() {
assert!(MacTimestamp(0) < MacTimestamp(1));
let mut v = [MacTimestamp(5), MacTimestamp(1), MacTimestamp(3)];
v.sort();
assert_eq!(v, [MacTimestamp(1), MacTimestamp(3), MacTimestamp(5)]);
}
}