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        // Add 5 minutes
72        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
73        assert_eq!(time.epoch_millis(), 300_000);
74    }
75
76    #[test]
77    #[should_panic(expected = "failed to get epoch time")]
78    fn test_epoch_millis_panics() {
79        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
80        time.epoch_millis();
81    }
82
83    #[test]
84    fn test_add_jittered() {
85        let mut rng = rand::thread_rng();
86        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
87        let jitter = Duration::from_secs(2);
88
89        // Ensure we generate values both below and above the average time.
90        let (mut below, mut above) = (false, false);
91        let avg = time + jitter;
92        for _ in 0..100 {
93            let new_time = time.add_jittered(&mut rng, jitter);
94
95            // Record values higher or lower than the average
96            below |= new_time < avg;
97            above |= new_time > avg;
98
99            // Check bounds
100            assert!(new_time >= time);
101            assert!(new_time <= time + (jitter * 2));
102        }
103        assert!(below && above);
104    }
105}