1use std::time::{SystemTime, UNIX_EPOCH};
6
7pub fn utc_timestamp_iso() -> String {
9 fmt_unix_secs(unix_secs_now())
10}
11
12pub fn unix_secs_now() -> u64 {
14 SystemTime::now()
15 .duration_since(UNIX_EPOCH)
16 .unwrap_or_default()
17 .as_secs()
18}
19
20pub fn fmt_unix_secs(s: u64) -> String {
22 let sec = s % 60;
23 let min = (s / 60) % 60;
24 let hour = (s / 3600) % 24;
25 let days = s / 86400;
26 let year_400 = days / 146097;
27 let rem = days % 146097;
28 let year_100 = rem / 36524;
29 let rem = rem % 36524;
30 let year_4 = rem / 1461;
31 let rem = rem % 1461;
32 let year_1 = rem / 365;
33 let year = 1970 + year_400 * 400 + year_100 * 100 + year_4 * 4 + year_1;
34 let doy = rem % 365;
35 let (month, day) = doy_to_md(doy, is_leap(year));
36 format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
37}
38
39fn is_leap(y: u64) -> bool {
40 (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
41}
42
43fn doy_to_md(doy: u64, leap: bool) -> (u64, u64) {
44 let days_in_month: [u64; 12] = if leap {
45 [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
46 } else {
47 [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
48 };
49 let mut rem = doy;
50 for (i, &dim) in days_in_month.iter().enumerate() {
51 if rem < dim {
52 return (i as u64 + 1, rem + 1);
53 }
54 rem -= dim;
55 }
56 (12, 31)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn epoch_formats_correctly() {
65 assert_eq!(fmt_unix_secs(0), "1970-01-01T00:00:00Z");
66 }
67
68 #[test]
69 fn known_date_formats_correctly() {
70 assert_eq!(fmt_unix_secs(1704067200), "2024-01-01T00:00:00Z");
72 }
73
74 #[test]
75 fn unix_secs_now_is_reasonable() {
76 let t = unix_secs_now();
77 assert!(t > 1_700_000_000, "timestamp must be after 2023");
78 }
79
80 #[test]
81 fn utc_timestamp_iso_parses() {
82 let s = utc_timestamp_iso();
83 assert_eq!(s.len(), 20);
84 assert!(s.ends_with('Z'));
85 }
86}