Skip to main content

pacenotes_installer/
utc_time.rs

1//! UTC dates as text, with Howard Hinnant's civil-from-days arithmetic,
2//! hand-rolled to spare a date crate.
3
4pub fn unix_now() -> i64 {
5    std::time::SystemTime::now()
6        .duration_since(std::time::UNIX_EPOCH)
7        .map(|elapsed| elapsed.as_secs() as i64)
8        .unwrap_or(0)
9}
10
11/// `2026-10-25T18-15-00Z`: sortable, and without ISO 8601's colons, which
12/// Windows forbids in a file or directory name.
13pub fn file_stamp(unix_seconds: i64) -> String {
14    let civil = Civil::of(unix_seconds);
15    format!(
16        "{}T{:02}-{:02}-{:02}Z",
17        civil.date(),
18        civil.hours,
19        civil.minutes,
20        civil.seconds
21    )
22}
23
24/// `2026-10-25T18:15:00Z`, the shape the server's log writes, so the two line up.
25pub fn iso_stamp(unix_seconds: i64) -> String {
26    let civil = Civil::of(unix_seconds);
27    format!(
28        "{}T{:02}:{:02}:{:02}Z",
29        civil.date(),
30        civil.hours,
31        civil.minutes,
32        civil.seconds
33    )
34}
35
36struct Civil {
37    year: i64,
38    month: i64,
39    day: i64,
40    hours: i64,
41    minutes: i64,
42    seconds: i64,
43}
44
45impl Civil {
46    fn of(unix_seconds: i64) -> Civil {
47        let days = unix_seconds.div_euclid(86_400);
48        let of_day = unix_seconds.rem_euclid(86_400);
49
50        let z = days + 719_468;
51        let era = z.div_euclid(146_097);
52        let doe = z.rem_euclid(146_097);
53        let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
54        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
55        let mp = (5 * doy + 2) / 153;
56        let month = if mp < 10 { mp + 3 } else { mp - 9 };
57        Civil {
58            year: yoe + era * 400 + i64::from(month <= 2),
59            month,
60            day: doy - (153 * mp + 2) / 5 + 1,
61            hours: of_day / 3600,
62            minutes: (of_day % 3600) / 60,
63            seconds: of_day % 60,
64        }
65    }
66
67    fn date(&self) -> String {
68        format!("{:04}-{:02}-{:02}", self.year, self.month, self.day)
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    /// Expected values come from .NET's `DateTimeOffset.FromUnixTimeSeconds`,
75    /// since a hand-worked expectation passes however broken the code is.
76    #[test]
77    fn stamps_match_the_utc_calendar() {
78        assert_eq!(super::file_stamp(0), "1970-01-01T00-00-00Z");
79        assert_eq!(super::file_stamp(1_761_000_000), "2025-10-20T22-40-00Z");
80        assert_eq!(super::file_stamp(1_709_164_800), "2024-02-29T00-00-00Z");
81        assert_eq!(super::iso_stamp(1_761_000_000), "2025-10-20T22:40:00Z");
82        assert_eq!(super::iso_stamp(1_709_164_799), "2024-02-28T23:59:59Z");
83    }
84
85    #[test]
86    fn file_stamps_sort_chronologically_and_are_legal_on_windows() {
87        let earlier = super::file_stamp(1_761_000_000);
88        let later = super::file_stamp(1_761_000_001);
89        assert!(earlier < later);
90        for forbidden in [':', '/', '\\', '*', '?', '"', '<', '>', '|'] {
91            assert!(!later.contains(forbidden), "{later}");
92        }
93    }
94}