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}