1use std::time::{SystemTime, UNIX_EPOCH};
2
3pub 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
18pub 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
28pub 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
46fn 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
61fn 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}