Skip to main content

isothrum_runner/
clock.rs

1//! Time, passed in: the runner reads the clock only through [`Clock`], so a test fixes it.
2
3use std::time::{SystemTime, UNIX_EPOCH};
4
5/// Where the runner reads the time (event and record timestamps).
6pub trait Clock: Send + Sync {
7    /// Now.
8    fn now(&self) -> SystemTime;
9}
10
11/// The system's clock.
12#[derive(Debug, Clone, Copy, Default)]
13pub struct SystemClock;
14
15impl Clock for SystemClock {
16    fn now(&self) -> SystemTime {
17        SystemTime::now()
18    }
19}
20
21/// A clock that always reads the same instant (feature `fake`).
22#[cfg(any(test, feature = "fake"))]
23#[derive(Debug, Clone, Copy)]
24pub struct FixedClock(pub SystemTime);
25
26#[cfg(any(test, feature = "fake"))]
27impl Clock for FixedClock {
28    fn now(&self) -> SystemTime {
29        self.0
30    }
31}
32
33/// `time` in whole seconds since the Unix epoch (0 before it).
34pub(crate) fn unix_seconds(time: SystemTime) -> u64 {
35    time.duration_since(UNIX_EPOCH).map_or(0, |d| d.as_secs())
36}
37
38/// `time` as `YYYY-MM-DDTHH:MM:SSZ` (UTC, whole seconds; before 1970 reads as 1970).
39#[must_use]
40pub fn rfc3339(time: SystemTime) -> String {
41    let secs = unix_seconds(time);
42    let (hour, minute, second) = ((secs % 86_400) / 3600, (secs % 3600) / 60, secs % 60);
43    let (year, month, day) = civil(secs / 86_400);
44    format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}Z")
45}
46
47/// Days since 1970-01-01 as a civil date (Howard Hinnant's algorithm, as Isoloom's registry wrote
48/// it; the short names are the published algorithm's own).
49fn civil(days: u64) -> (i64, i64, i64) {
50    let z = days.cast_signed() + 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 d = doy - (153 * mp + 2) / 5 + 1;
57    let mo = if mp < 10 { mp + 3 } else { mp - 9 };
58    let y = yoe + era * 400 + i64::from(mo <= 2);
59    (y, mo, d)
60}
61
62#[cfg(test)]
63mod tests {
64    use std::time::Duration;
65
66    use super::*;
67
68    #[test]
69    fn times_read_as_rfc3339_in_utc() {
70        assert_eq!(rfc3339(UNIX_EPOCH), "1970-01-01T00:00:00Z");
71        assert_eq!(rfc3339(UNIX_EPOCH + Duration::from_secs(1_792_065_600)), "2026-10-15T12:00:00Z");
72        assert_eq!(rfc3339(UNIX_EPOCH + Duration::from_secs(951_782_400)), "2000-02-29T00:00:00Z");
73        assert_eq!(rfc3339(UNIX_EPOCH - Duration::from_secs(5)), "1970-01-01T00:00:00Z");
74    }
75
76    #[test]
77    fn a_fixed_clock_stands_still() {
78        let clock = FixedClock(UNIX_EPOCH + Duration::from_secs(60));
79        assert_eq!(clock.now(), clock.now());
80        assert_eq!(rfc3339(clock.now()), "1970-01-01T00:01:00Z");
81    }
82}