rovenue/
time.rs

1use std::time::{Duration, SystemTime, UNIX_EPOCH};
2
3pub trait Clock: Send + Sync {
4    fn now_unix_ms(&self) -> u64;
5    fn sleep(&self, d: Duration);
6}
7
8pub struct SystemClock;
9
10impl Clock for SystemClock {
11    fn now_unix_ms(&self) -> u64 {
12        SystemTime::now()
13            .duration_since(UNIX_EPOCH)
14            .map(|d| d.as_millis() as u64)
15            .unwrap_or(0)
16    }
17
18    fn sleep(&self, d: Duration) {
19        std::thread::sleep(d);
20    }
21}
22
23/// Unix millis → `YYYY-MM-DDTHH:MM:SS.sssZ` (UTC), no date crate needed.
24///
25/// Device-emitted envelopes stamp their own `occurred_at` (the façades
26/// never pass one), and the server's envelope schema requires an ISO-8601
27/// datetime — so the formatter lives next to the clock. civil-date math
28/// is Howard Hinnant's days-to-ymd; the output also satisfies api.rs's
29/// `is_plausible_iso8601` gate on the enqueue path.
30pub fn rfc3339_millis(unix_ms: u64) -> String {
31    let secs = unix_ms / 1000;
32    let millis = unix_ms % 1000;
33    let days = (secs / 86_400) as i64;
34    let secs_of_day = secs % 86_400;
35
36    // days since civil 0000-03-01 → (y, m, d).
37    let z = days + 719_468;
38    let era = z.div_euclid(146_097);
39    let doe = z - era * 146_097;
40    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
41    let y = yoe + era * 400;
42    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
43    let mp = (5 * doy + 2) / 153;
44    let d = doy - (153 * mp + 2) / 5 + 1;
45    let m = if mp < 10 { mp + 3 } else { mp - 9 };
46    let y = if m <= 2 { y + 1 } else { y };
47
48    format!(
49        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
50        y,
51        m,
52        d,
53        secs_of_day / 3600,
54        (secs_of_day % 3600) / 60,
55        secs_of_day % 60,
56        millis
57    )
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn epoch_formats_with_zero_millis() {
66        assert_eq!(rfc3339_millis(0), "1970-01-01T00:00:00.000Z");
67    }
68
69    #[test]
70    fn whole_seconds_gain_zero_millis() {
71        // `date -u -r 1758672000` — independent of this implementation.
72        assert_eq!(
73            rfc3339_millis(1_758_672_000_000),
74            "2025-09-24T00:00:00.000Z"
75        );
76    }
77
78    #[test]
79    fn sub_second_millis_survive() {
80        assert_eq!(
81            rfc3339_millis(1_758_672_000_123),
82            "2025-09-24T00:00:00.123Z"
83        );
84    }
85
86    #[test]
87    fn leap_day_and_day_rollover() {
88        // Via python datetime, independent of this implementation.
89        assert_eq!(
90            rfc3339_millis(1_709_164_800_000),
91            "2024-02-29T00:00:00.000Z"
92        );
93        assert_eq!(
94            rfc3339_millis(1_709_251_200_000),
95            "2024-03-01T00:00:00.000Z"
96        );
97    }
98}