Skip to main content

mem7_datetime/
lib.rs

1use std::time::{SystemTime, UNIX_EPOCH};
2
3/// Current UTC time as ISO 8601 string (`YYYY-MM-DDThh:mm:ssZ`).
4pub fn now_iso() -> String {
5    let d = SystemTime::now()
6        .duration_since(UNIX_EPOCH)
7        .unwrap_or_default();
8    let secs = d.as_secs();
9    let days = secs / 86400;
10    let time_secs = secs % 86400;
11    let hours = time_secs / 3600;
12    let minutes = (time_secs % 3600) / 60;
13    let seconds = time_secs % 60;
14    let (year, month, day) = days_to_ymd(days);
15    format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
16}
17
18/// Current UTC date as `YYYY-MM-DD`.
19pub fn today_date() -> String {
20    let d = SystemTime::now()
21        .duration_since(UNIX_EPOCH)
22        .unwrap_or_default();
23    let days = d.as_secs() / 86400;
24    let (y, m, d) = days_to_ymd(days);
25    format!("{y:04}-{m:02}-{d:02}")
26}
27
28/// Parse ISO 8601 `YYYY-MM-DDThh:mm:ssZ` into epoch seconds.
29pub fn iso_to_epoch(ts: &str) -> Option<f64> {
30    let ts = ts.trim().trim_end_matches('Z');
31    let (date, time) = ts.split_once('T')?;
32    let mut date_parts = date.split('-');
33    let y: u64 = date_parts.next()?.parse().ok()?;
34    let m: u64 = date_parts.next()?.parse().ok()?;
35    let d: u64 = date_parts.next()?.parse().ok()?;
36
37    let mut time_parts = time.split(':');
38    let h: u64 = time_parts.next()?.parse().ok()?;
39    let min: u64 = time_parts.next()?.parse().ok()?;
40    let sec: u64 = time_parts.next()?.parse().ok()?;
41
42    let days = ymd_to_days(y, m, d);
43    Some((days * 86400 + h * 3600 + min * 60 + sec) as f64)
44}
45
46/// Convert days since Unix epoch → (year, month, day) using the civil calendar algorithm.
47fn days_to_ymd(days_since_epoch: u64) -> (u64, u64, u64) {
48    let z = days_since_epoch + 719468;
49    let era = z / 146097;
50    let doe = z - era * 146097;
51    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
52    let y = yoe + era * 400;
53    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
54    let mp = (5 * doy + 2) / 153;
55    let d = doy - (153 * mp + 2) / 5 + 1;
56    let m = if mp < 10 { mp + 3 } else { mp - 9 };
57    let y = if m <= 2 { y + 1 } else { y };
58    (y, m, d)
59}
60
61/// Convert (year, month, day) → days since Unix epoch.
62fn ymd_to_days(y: u64, m: u64, d: u64) -> u64 {
63    let y = if m <= 2 { y.wrapping_sub(1) } else { y };
64    let era = y / 400;
65    let yoe = y - era * 400;
66    let m_adj = if m > 2 { m - 3 } else { m + 9 };
67    let doy = (153 * m_adj + 2) / 5 + d - 1;
68    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
69    era * 146097 + doe - 719468
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn now_iso_is_well_formed() {
78        let s = now_iso();
79        assert!(s.ends_with('Z'));
80        assert!(s.contains('T'));
81        assert_eq!(s.len(), 20);
82    }
83
84    #[test]
85    fn today_date_is_well_formed() {
86        let s = today_date();
87        assert_eq!(s.len(), 10);
88        assert_eq!(&s[4..5], "-");
89        assert_eq!(&s[7..8], "-");
90    }
91
92    #[test]
93    fn iso_round_trip() {
94        let epoch = iso_to_epoch("2025-01-01T00:00:00Z");
95        assert!(epoch.is_some());
96        assert!((epoch.unwrap() - 1735689600.0).abs() < 1.0);
97    }
98
99    #[test]
100    fn iso_to_epoch_rejects_invalid() {
101        assert!(iso_to_epoch("not-a-date").is_none());
102        assert!(iso_to_epoch("").is_none());
103    }
104}