commonware_utils/
time.rs

1//! Utility functions for `std::time`.
2
3use rand::Rng;
4use std::time::{Duration, SystemTime};
5
6/// Extension trait to add methods to `std::time::SystemTime`
7pub trait SystemTimeExt {
8    /// Returns the duration since the Unix epoch.
9    ///
10    /// Panics if the system time is before the Unix epoch.
11    fn epoch(&self) -> Duration;
12
13    /// Returns the number of milliseconds (rounded down) since the Unix epoch.
14    ///
15    /// Panics if the system time is before the Unix epoch.
16    /// Saturates at `u64::MAX`.
17    fn epoch_millis(&self) -> u64;
18
19    /// Adds a random `Duration` to the current time between `0` and `jitter * 2` and returns the
20    /// resulting `SystemTime`. The random duration is generated using the provided `context`.
21    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
22}
23
24impl SystemTimeExt for SystemTime {
25    fn epoch(&self) -> Duration {
26        self.duration_since(std::time::UNIX_EPOCH)
27            .expect("failed to get epoch time")
28    }
29
30    fn epoch_millis(&self) -> u64 {
31        self.epoch().as_millis().min(u64::MAX as u128) as u64
32    }
33
34    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
35        *self + rng.gen_range(Duration::default()..=jitter * 2)
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn test_epoch() {
45        let time = SystemTime::UNIX_EPOCH;
46        assert_eq!(time.epoch(), Duration::from_secs(0));
47
48        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
49        assert_eq!(time.epoch(), Duration::from_millis(1_001));
50    }
51
52    #[test]
53    #[should_panic(expected = "failed to get epoch time")]
54    fn test_epoch_panics() {
55        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
56        time.epoch();
57    }
58
59    #[test]
60    fn test_epoch_millis() {
61        let time = SystemTime::UNIX_EPOCH;
62        assert_eq!(time.epoch_millis(), 0);
63
64        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
65        assert_eq!(time.epoch_millis(), 1_001);
66
67        // Rounds nanoseconds down
68        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
69        assert_eq!(time.epoch_millis(), 1_000);
70
71        // Saturates at u64::MAX
72        let time = SystemTime::UNIX_EPOCH + Duration::from_millis(u64::MAX);
73        assert_eq!(time.epoch_millis(), u64::MAX);
74        let time = time + Duration::from_millis(1);
75        assert_eq!(time.epoch_millis(), u64::MAX);
76    }
77
78    #[test]
79    #[should_panic(expected = "failed to get epoch time")]
80    fn test_epoch_millis_panics() {
81        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
82        time.epoch_millis();
83    }
84
85    #[test]
86    fn test_add_jittered() {
87        let mut rng = rand::thread_rng();
88        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
89        let jitter = Duration::from_secs(2);
90
91        // Ensure we generate values both below and above the average time.
92        let (mut below, mut above) = (false, false);
93        for _ in 0..100 {
94            let new_time = time.add_jittered(&mut rng, jitter);
95
96            // Record values higher or lower than the average
97            let avg = time + jitter;
98            below |= new_time < avg;
99            above |= new_time > avg;
100
101            // Check bounds
102            assert!(new_time >= time);
103            assert!(new_time <= time + (jitter * 2));
104        }
105        assert!(below && above);
106    }
107}