Skip to main content

epics_libcom_rs/
walltime.rs

1//! [`WallTime`] — a wall-clock instant with full EPICS nanosecond precision
2//! on every platform.
3
4use std::time::{Duration, SystemTime};
5
6/// Wall-clock instant carrying the full EPICS `nsec` (1 ns precision) on every
7/// platform.
8///
9/// Stored as a [`Duration`] since the Unix epoch (integer seconds + nanos), so
10/// a wire-sourced `epicsTimeStamp.nsec` survives a store→serve round-trip
11/// exactly. [`std::time::SystemTime`] is backed by `FILETIME` on Windows and
12/// only resolves to 100 ns, which truncated the low nanosecond digits of an
13/// externally supplied timestamp (a PVA PUT, a gateway pass-through, a
14/// `Q:time:tag` nsec split); `WallTime` keeps integers end-to-end and never
15/// truncates. On Linux/macOS `SystemTime` already holds 1 ns, so this only
16/// changes behaviour on Windows.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub struct WallTime {
19    since_epoch: Duration,
20}
21
22impl WallTime {
23    /// The Unix epoch (1970-01-01T00:00:00Z) — the zero instant.
24    pub const UNIX_EPOCH: Self = Self {
25        since_epoch: Duration::ZERO,
26    };
27
28    /// Current wall-clock time from the OS clock.
29    ///
30    /// On Windows the OS clock itself is 100 ns-granular (the same limit C
31    /// EPICS hits), so "now" loses no precision relative to C; only an
32    /// externally supplied sub-100 ns `nsec` would, and that arrives through
33    /// [`WallTime::from_unix`].
34    pub fn now() -> Self {
35        SystemTime::now().into()
36    }
37
38    /// Build from an integer `(unix_seconds, nanoseconds)` pair. `nanos` at or
39    /// above 1e9 carries into seconds, matching [`Duration::new`].
40    pub fn from_unix(secs: u64, nanos: u32) -> Self {
41        Self {
42            since_epoch: Duration::new(secs, nanos),
43        }
44    }
45
46    /// Time elapsed since the Unix epoch as a [`Duration`] (1 ns precision).
47    pub fn since_unix_epoch(self) -> Duration {
48        self.since_epoch
49    }
50
51    /// Whole seconds since the Unix epoch.
52    pub fn unix_secs(self) -> u64 {
53        self.since_epoch.as_secs()
54    }
55
56    /// Sub-second nanoseconds in `0..1_000_000_000`.
57    pub fn subsec_nanos(self) -> u32 {
58        self.since_epoch.subsec_nanos()
59    }
60
61    /// This instant minus `d`, saturating at [`WallTime::UNIX_EPOCH`] rather
62    /// than panicking on underflow.
63    pub fn saturating_sub(self, d: Duration) -> Self {
64        Self {
65            since_epoch: self.since_epoch.saturating_sub(d),
66        }
67    }
68}
69
70impl From<SystemTime> for WallTime {
71    /// Times before the Unix epoch clamp to [`WallTime::UNIX_EPOCH`]
72    /// (`unwrap_or_default`), matching the previous
73    /// `duration_since(UNIX_EPOCH).unwrap_or(ZERO)` behaviour at the
74    /// snapshot/codec boundaries.
75    fn from(t: SystemTime) -> Self {
76        Self {
77            since_epoch: t.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_default(),
78        }
79    }
80}
81
82impl From<WallTime> for SystemTime {
83    /// For display/formatting interop (e.g. `chrono`). On Windows this
84    /// re-introduces the platform's 100 ns `SystemTime` granularity, so use
85    /// the integer accessors ([`WallTime::unix_secs`] / [`WallTime::subsec_nanos`])
86    /// when the full `nsec` must be preserved.
87    fn from(t: WallTime) -> Self {
88        SystemTime::UNIX_EPOCH + t.since_epoch
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn from_unix_preserves_full_nanoseconds() {
98        // The exact value that std SystemTime truncates to 100 ns on Windows.
99        let t = WallTime::from_unix(42, 123_456_789);
100        assert_eq!(t.unix_secs(), 42);
101        assert_eq!(t.subsec_nanos(), 123_456_789);
102        assert_eq!(t.since_unix_epoch(), Duration::new(42, 123_456_789));
103    }
104
105    #[test]
106    fn systemtime_round_trip_through_unix_epoch() {
107        let st = SystemTime::UNIX_EPOCH + Duration::from_secs(1_700_000_000);
108        let wt: WallTime = st.into();
109        assert_eq!(wt.unix_secs(), 1_700_000_000);
110        let back: SystemTime = wt.into();
111        assert_eq!(back, st);
112    }
113
114    #[test]
115    fn pre_epoch_systemtime_clamps_to_epoch() {
116        let before = SystemTime::UNIX_EPOCH - Duration::from_secs(5);
117        let wt: WallTime = before.into();
118        assert_eq!(wt, WallTime::UNIX_EPOCH);
119    }
120
121    #[test]
122    fn saturating_sub_floors_at_epoch() {
123        let t = WallTime::from_unix(0, 500);
124        assert_eq!(
125            t.saturating_sub(Duration::from_secs(1)),
126            WallTime::UNIX_EPOCH
127        );
128        assert_eq!(
129            WallTime::from_unix(10, 0).saturating_sub(Duration::from_secs(3)),
130            WallTime::from_unix(7, 0)
131        );
132    }
133
134    #[test]
135    fn ordering_matches_chronology() {
136        assert!(WallTime::from_unix(1, 0) < WallTime::from_unix(1, 1));
137        assert!(WallTime::from_unix(2, 0) > WallTime::from_unix(1, 999_999_999));
138        assert_eq!(WallTime::UNIX_EPOCH, WallTime::from_unix(0, 0));
139    }
140}