1pub fn utc_now_iso8601() -> String {
3 let secs = std::time::SystemTime::now()
4 .duration_since(std::time::UNIX_EPOCH)
5 .unwrap_or_default()
6 .as_secs();
7 unix_secs_to_iso8601(secs)
8}
9
10pub fn iso8601_to_filename_safe(ts: &str) -> String {
14 ts.replace([':', '-', 'T', 'Z'], "")
15}
16
17pub fn utc_now_filename_safe() -> String {
19 iso8601_to_filename_safe(&utc_now_iso8601())
20}
21
22pub fn unix_secs_now() -> u64 {
24 std::time::SystemTime::now()
25 .duration_since(std::time::UNIX_EPOCH)
26 .unwrap_or_default()
27 .as_secs()
28}
29
30pub fn unix_secs_to_iso8601(secs: u64) -> String {
32 let days = secs / 86400;
33 let time_of_day = secs % 86400;
34 let hours = time_of_day / 3600;
35 let minutes = (time_of_day % 3600) / 60;
36 let seconds = time_of_day % 60;
37
38 let (year, month, day) = days_to_ymd(days);
39
40 format!("{year:04}-{month:02}-{day:02}T{hours:02}:{minutes:02}:{seconds:02}Z")
41}
42
43fn days_to_ymd(days: u64) -> (u64, u64, u64) {
44 let z = days + 719468;
46 let era = z / 146097;
47 let doe = z - era * 146097;
48 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
49 let y = yoe + era * 400;
50 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
51 let mp = (5 * doy + 2) / 153;
52 let d = doy - (153 * mp + 2) / 5 + 1;
53 let m = if mp < 10 { mp + 3 } else { mp - 9 };
54 let y = if m <= 2 { y + 1 } else { y };
55 (y, m, d)
56}
57
58pub fn parse_duration_str(s: &str) -> Result<std::time::Duration, String> {
62 let s = s.trim();
63 const SUFFIXES: &[(char, u64)] = &[('s', 1), ('m', 60), ('h', 3600), ('d', 86400)];
64 for &(suffix, multiplier) in SUFFIXES {
65 if let Some(n) = s.strip_suffix(suffix) {
66 return n
67 .trim()
68 .parse::<u64>()
69 .map(|v| std::time::Duration::from_secs(v * multiplier))
70 .map_err(|_| format!("invalid timeout: {}", s));
71 }
72 }
73 s.parse::<u64>()
74 .map(std::time::Duration::from_secs)
75 .map_err(|_| format!("invalid timeout '{}': use 30s, 5m, or 1h", s))
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn iso8601_to_filename_safe_strips_separators() {
84 assert_eq!(
85 iso8601_to_filename_safe("2026-05-12T14:30:25Z"),
86 "20260512143025"
87 );
88 }
89
90 #[test]
91 fn iso8601_to_filename_safe_preserves_fractional_seconds() {
92 assert_eq!(
94 iso8601_to_filename_safe("2026-05-12T14:30:25.123Z"),
95 "20260512143025.123"
96 );
97 }
98
99 #[test]
100 fn utc_now_filename_safe_has_no_unsafe_chars() {
101 let s = utc_now_filename_safe();
102 assert!(!s.is_empty());
103 assert!(
104 !s.contains([':', '-', 'T', 'Z']),
105 "filename-safe stamp contained banned char: {s:?}"
106 );
107 }
108}