Skip to main content

commonware_utils/
time.rs

1//! Utility functions for `std::time`.
2
3use rand::{Rng, RngExt as _};
4use std::time::{Duration, SystemTime};
5
6/// Number of nanoseconds in a second.
7pub const NANOS_PER_SEC: u128 = 1_000_000_000;
8
9/// Maximum duration that can be safely added to [`SystemTime::UNIX_EPOCH`] without overflow on the
10/// current platform.
11///
12/// Source: `SystemTime` on Unix stores seconds in a signed 64-bit integer; see
13/// [`std::sys::pal::unix::time`](https://github.com/rust-lang/rust/blob/master/library/std/src/sys/pal/unix/time.rs),
14/// which bounds additions at `i64::MAX` seconds plus 999_999_999 nanoseconds.
15pub const MAX_DURATION_SINCE_UNIX_EPOCH: Duration = Duration::new(i64::MAX as u64, 999_999_999);
16
17/// The precision of [`SystemTime`] on supported platforms.
18pub const SYSTEM_TIME_PRECISION: Duration = Duration::from_nanos(1);
19
20/// Extension trait providing additional functionality for [`Duration`].
21pub trait DurationExt {
22    /// Returns the number of whole milliseconds as a `u64`.
23    ///
24    /// # Panics
25    ///
26    /// Panics if the number of whole milliseconds exceeds `u64::MAX`.
27    fn as_millis_u64(&self) -> u64;
28
29    /// Creates a duration from nanoseconds represented as a `u128`. Saturates anything beyond the
30    /// representable range.
31    fn from_nanos_saturating(ns: u128) -> Duration;
32
33    /// Parse a duration string with time unit suffixes.
34    ///
35    /// This function accepts duration strings with the following suffixes:
36    /// - `ms`: Milliseconds (e.g., "500ms", "1000ms")
37    /// - `s`: Seconds (e.g., "30s", "5s")
38    /// - `m`: Minutes (e.g., "2m", "30m")
39    /// - `h`: Hours (e.g., "1h", "24h")
40    ///
41    /// A suffix is required - strings without suffixes will return an error.
42    ///
43    /// # Overflow Protection
44    ///
45    /// The function includes overflow protection for time unit conversions:
46    /// - Hours are safely converted to seconds (hours * 3600) with overflow checking
47    /// - Minutes are safely converted to seconds (minutes * 60) with overflow checking
48    /// - Values that would cause integer overflow return an error
49    ///
50    /// # Arguments
51    ///
52    /// * `s` - A string slice containing the duration with required suffix
53    ///
54    /// # Returns
55    ///
56    /// * `Ok(Duration)` - Successfully parsed duration
57    /// * `Err(String)` - Error message describing what went wrong (invalid format, overflow, etc.)
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// # use commonware_utils::DurationExt;
63    /// # use std::time::Duration;
64    ///
65    /// // Different time units
66    /// assert_eq!(Duration::parse("500ms").unwrap(), Duration::from_millis(500));
67    /// assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
68    /// assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
69    /// assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
70    ///
71    /// // Error cases
72    /// assert!(Duration::parse("invalid").is_err());
73    /// assert!(Duration::parse("10x").is_err());
74    /// assert!(Duration::parse("5minutes").is_err()); // Long forms not supported
75    /// assert!(Duration::parse("60").is_err()); // No suffix required
76    ///
77    /// // Overflow protection
78    /// let max_hours = u64::MAX / 3600;
79    /// assert!(Duration::parse(&format!("{}h", max_hours)).is_ok());      // At limit
80    /// assert!(Duration::parse(&format!("{}h", max_hours + 1)).is_err()); // Overflow
81    /// ```
82    fn parse(s: &str) -> Result<Duration, String>;
83}
84
85impl DurationExt for Duration {
86    fn as_millis_u64(&self) -> u64 {
87        u64::try_from(self.as_millis()).expect("duration milliseconds should fit in u64")
88    }
89
90    fn from_nanos_saturating(ns: u128) -> Duration {
91        // Clamp anything beyond the representable range
92        if ns > Self::MAX.as_nanos() {
93            return Self::MAX;
94        }
95
96        // Convert to `Duration`
97        let secs = (ns / NANOS_PER_SEC) as u64;
98        let nanos = (ns % NANOS_PER_SEC) as u32;
99        Self::new(secs, nanos)
100    }
101
102    fn parse(s: &str) -> Result<Duration, String> {
103        let s = s.trim();
104
105        // Handle milliseconds
106        if let Some(num_str) = s.strip_suffix("ms") {
107            let millis: u64 = num_str
108                .trim()
109                .parse()
110                .map_err(|_| format!("Invalid milliseconds value: '{num_str}'"))?;
111            return Ok(Self::from_millis(millis));
112        }
113
114        // Handle hours
115        if let Some(num_str) = s.strip_suffix("h") {
116            let hours: u64 = num_str
117                .trim()
118                .parse()
119                .map_err(|_| format!("Invalid hours value: '{num_str}'"))?;
120            let seconds = hours
121                .checked_mul(3600)
122                .ok_or_else(|| format!("Hours value too large (would overflow): '{hours}'"))?;
123            return Ok(Self::from_secs(seconds));
124        }
125
126        // Handle minutes
127        if let Some(num_str) = s.strip_suffix("m") {
128            let minutes: u64 = num_str
129                .trim()
130                .parse()
131                .map_err(|_| format!("Invalid minutes value: '{num_str}'"))?;
132            let seconds = minutes
133                .checked_mul(60)
134                .ok_or_else(|| format!("Minutes value too large (would overflow): '{minutes}'"))?;
135            return Ok(Self::from_secs(seconds));
136        }
137
138        // Handle seconds
139        if let Some(num_str) = s.strip_suffix("s") {
140            let secs: u64 = num_str
141                .trim()
142                .parse()
143                .map_err(|_| format!("Invalid seconds value: '{num_str}'"))?;
144            return Ok(Self::from_secs(secs));
145        }
146
147        // No suffix - return error
148        Err(format!(
149            "Invalid duration format: '{s}'. A suffix is required. \
150         Supported formats: '123ms', '30s', '5m', '2h'"
151        ))
152    }
153}
154
155/// Extension trait to add methods to `std::time::SystemTime`
156pub trait SystemTimeExt {
157    /// Returns the duration since the Unix epoch.
158    ///
159    /// Panics if the system time is before the Unix epoch.
160    fn epoch(&self) -> Duration;
161
162    /// Returns the number of milliseconds (rounded down) since the Unix epoch.
163    ///
164    /// Panics if the system time is before the Unix epoch.
165    /// Saturates at `u64::MAX`.
166    fn epoch_millis(&self) -> u64;
167
168    /// Adds a random `Duration` to the current time between `0` and `jitter * 2` and returns the
169    /// resulting `SystemTime`, saturating at the platform maximum. The random duration is
170    /// generated using the provided `context`.
171    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
172
173    /// Returns the maximum representable [SystemTime] on this platform.
174    fn limit() -> SystemTime;
175
176    /// Adds `delta` to the current time, saturating at the platform maximum instead of overflowing.
177    fn saturating_add_ext(&self, delta: Duration) -> SystemTime;
178}
179
180impl SystemTimeExt for SystemTime {
181    fn epoch(&self) -> Duration {
182        self.duration_since(std::time::UNIX_EPOCH)
183            .expect("failed to get epoch time")
184    }
185
186    fn epoch_millis(&self) -> u64 {
187        self.epoch().as_millis().min(u64::MAX as u128) as u64
188    }
189
190    fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
191        self.saturating_add_ext(rng.random_range(Duration::default()..=jitter * 2))
192    }
193
194    fn limit() -> SystemTime {
195        Self::UNIX_EPOCH
196            .checked_add(MAX_DURATION_SINCE_UNIX_EPOCH)
197            .expect("maximum system time must be representable")
198    }
199
200    fn saturating_add_ext(&self, delta: Duration) -> SystemTime {
201        if delta.is_zero() {
202            return *self;
203        }
204
205        // When adding less than SYSTEM_TIME_PRECISION, this may actually not fail but simply
206        // round down to the nearest representable value
207        self.checked_add(delta).unwrap_or_else(Self::limit)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::test_rng;
215
216    #[test]
217    fn test_as_millis_u64() {
218        for (duration, expected) in [
219            (Duration::ZERO, 0),
220            (Duration::from_nanos(999_999), 0),
221            (Duration::new(1, 234_567_890), 1_234),
222            (Duration::from_millis(u64::MAX), u64::MAX),
223            (
224                Duration::from_millis(u64::MAX) + Duration::from_nanos(999_999),
225                u64::MAX,
226            ),
227        ] {
228            assert_eq!(duration.as_millis_u64(), expected);
229        }
230    }
231
232    #[test]
233    fn test_as_millis_u64_overflow() {
234        for duration in [
235            Duration::from_millis(u64::MAX) + Duration::from_millis(1),
236            Duration::MAX,
237        ] {
238            assert!(std::panic::catch_unwind(|| duration.as_millis_u64()).is_err());
239        }
240    }
241
242    #[test]
243    fn test_epoch() {
244        let time = SystemTime::UNIX_EPOCH;
245        assert_eq!(time.epoch(), Duration::from_secs(0));
246
247        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
248        assert_eq!(time.epoch(), Duration::from_millis(1_001));
249    }
250
251    #[test]
252    #[should_panic(expected = "failed to get epoch time")]
253    fn test_epoch_panics() {
254        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
255        time.epoch();
256    }
257
258    #[test]
259    fn test_epoch_millis() {
260        let time = SystemTime::UNIX_EPOCH;
261        assert_eq!(time.epoch_millis(), 0);
262
263        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
264        assert_eq!(time.epoch_millis(), 1_001);
265
266        // Rounds nanoseconds down
267        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
268        assert_eq!(time.epoch_millis(), 1_000);
269
270        // Add 5 minutes
271        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
272        assert_eq!(time.epoch_millis(), 300_000);
273    }
274
275    #[test]
276    #[should_panic(expected = "failed to get epoch time")]
277    fn test_epoch_millis_panics() {
278        let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
279        time.epoch_millis();
280    }
281
282    #[test]
283    fn test_from_nanos_saturating() {
284        // Support simple cases
285        assert_eq!(Duration::from_nanos_saturating(0), Duration::new(0, 0));
286        assert_eq!(
287            Duration::from_nanos_saturating(NANOS_PER_SEC - 1),
288            Duration::new(0, (NANOS_PER_SEC - 1) as u32)
289        );
290        assert_eq!(
291            Duration::from_nanos_saturating(NANOS_PER_SEC + 1),
292            Duration::new(1, 1)
293        );
294
295        // Support larger values than `Duration::from_nanos`
296        let std = Duration::from_nanos(u64::MAX);
297        let beyond_std = Duration::from_nanos_saturating(u64::MAX as u128 + 1);
298        assert!(beyond_std > std);
299
300        // Test very large values
301        assert_eq!(
302            Duration::from_nanos_saturating(Duration::MAX.as_nanos()),
303            Duration::MAX
304        );
305
306        // Clamp anything beyond the representable range
307        assert_eq!(
308            Duration::from_nanos_saturating(Duration::MAX.as_nanos() + 1),
309            Duration::MAX
310        );
311        assert_eq!(Duration::from_nanos_saturating(u128::MAX), Duration::MAX);
312    }
313
314    #[test]
315    fn test_add_jittered() {
316        let mut rng = test_rng();
317        let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
318        let jitter = Duration::from_secs(2);
319
320        // Ensure we generate values both below and above the average time.
321        let (mut below, mut above) = (false, false);
322        let avg = time + jitter;
323        for _ in 0..100 {
324            let new_time = time.add_jittered(&mut rng, jitter);
325
326            // Record values higher or lower than the average
327            below |= new_time < avg;
328            above |= new_time > avg;
329
330            // Check bounds
331            assert!(new_time >= time);
332            assert!(new_time <= time + (jitter * 2));
333        }
334        assert!(below && above);
335    }
336
337    #[test]
338    fn test_add_jittered_saturates() {
339        let mut rng = test_rng();
340        let time = SystemTime::limit() - Duration::from_secs(1);
341        let jitter = Duration::from_secs(60);
342
343        for _ in 0..100 {
344            let new_time = time.add_jittered(&mut rng, jitter);
345            assert!(new_time >= time);
346            assert!(new_time <= SystemTime::limit());
347        }
348    }
349
350    #[test]
351    fn check_duration_limit() {
352        // Rollback to limit
353        let result = SystemTime::limit()
354            .checked_add(SYSTEM_TIME_PRECISION - Duration::from_nanos(1))
355            .expect("addition within precision should round down");
356        assert_eq!(result, SystemTime::limit(), "unexpected precision");
357
358        // Exceed limit
359        let result = SystemTime::limit().checked_add(SYSTEM_TIME_PRECISION);
360        assert!(result.is_none(), "able to exceed max duration");
361    }
362
363    #[test]
364    fn system_time_saturating_add() {
365        let max = SystemTime::limit();
366        assert_eq!(max.saturating_add_ext(Duration::from_nanos(1)), max);
367        assert_eq!(max.saturating_add_ext(Duration::from_secs(1)), max);
368    }
369
370    #[test]
371    fn test_duration_parse_milliseconds() {
372        assert_eq!(
373            Duration::parse("500ms").unwrap(),
374            Duration::from_millis(500)
375        );
376        assert_eq!(Duration::parse("0ms").unwrap(), Duration::from_millis(0));
377        assert_eq!(Duration::parse("1ms").unwrap(), Duration::from_millis(1));
378        assert_eq!(
379            Duration::parse("1000ms").unwrap(),
380            Duration::from_millis(1000)
381        );
382        assert_eq!(
383            Duration::parse("250ms").unwrap(),
384            Duration::from_millis(250)
385        );
386    }
387
388    #[test]
389    fn test_duration_parse_seconds() {
390        assert_eq!(Duration::parse("30s").unwrap(), Duration::from_secs(30));
391        assert_eq!(Duration::parse("0s").unwrap(), Duration::from_secs(0));
392        assert_eq!(Duration::parse("1s").unwrap(), Duration::from_secs(1));
393        assert_eq!(Duration::parse("45s").unwrap(), Duration::from_secs(45));
394        assert_eq!(Duration::parse("60s").unwrap(), Duration::from_secs(60));
395        assert_eq!(Duration::parse("3600s").unwrap(), Duration::from_secs(3600));
396    }
397
398    #[test]
399    fn test_duration_parse_minutes() {
400        assert_eq!(Duration::parse("5m").unwrap(), Duration::from_secs(300));
401        assert_eq!(Duration::parse("1m").unwrap(), Duration::from_secs(60));
402        assert_eq!(Duration::parse("0m").unwrap(), Duration::from_secs(0));
403        assert_eq!(Duration::parse("10m").unwrap(), Duration::from_secs(600));
404        assert_eq!(Duration::parse("15m").unwrap(), Duration::from_secs(900));
405        assert_eq!(Duration::parse("30m").unwrap(), Duration::from_secs(1800));
406        assert_eq!(Duration::parse("60m").unwrap(), Duration::from_secs(3600));
407    }
408
409    #[test]
410    fn test_duration_parse_hours() {
411        assert_eq!(Duration::parse("2h").unwrap(), Duration::from_secs(7200));
412        assert_eq!(Duration::parse("1h").unwrap(), Duration::from_secs(3600));
413        assert_eq!(Duration::parse("0h").unwrap(), Duration::from_secs(0));
414        assert_eq!(Duration::parse("3h").unwrap(), Duration::from_secs(10800));
415        assert_eq!(Duration::parse("4h").unwrap(), Duration::from_secs(14400));
416        assert_eq!(Duration::parse("12h").unwrap(), Duration::from_secs(43200));
417        assert_eq!(Duration::parse("24h").unwrap(), Duration::from_secs(86400));
418        assert_eq!(
419            Duration::parse("168h").unwrap(),
420            Duration::from_secs(604800)
421        );
422        // 1 week
423    }
424
425    #[test]
426    fn test_duration_parse_whitespace() {
427        // Should handle whitespace around the input
428        assert_eq!(Duration::parse("  30s  ").unwrap(), Duration::from_secs(30));
429        assert_eq!(
430            Duration::parse("\t500ms\n").unwrap(),
431            Duration::from_millis(500)
432        );
433        assert_eq!(Duration::parse(" 2h ").unwrap(), Duration::from_secs(7200));
434
435        // Should handle whitespace between number and suffix
436        assert_eq!(Duration::parse("30 s").unwrap(), Duration::from_secs(30));
437        assert_eq!(
438            Duration::parse("500 ms").unwrap(),
439            Duration::from_millis(500)
440        );
441        assert_eq!(Duration::parse("2 h").unwrap(), Duration::from_secs(7200));
442        assert_eq!(Duration::parse("5 m").unwrap(), Duration::from_secs(300));
443    }
444
445    #[test]
446    fn test_duration_parse_error_cases() {
447        // Invalid number
448        assert!(Duration::parse("invalid").is_err());
449        assert!(Duration::parse("abc123ms").is_err());
450        assert!(Duration::parse("12.5s").is_err()); // Decimal not supported
451
452        // Invalid suffix
453        assert!(Duration::parse("10x").is_err());
454        assert!(Duration::parse("30days").is_err());
455        assert!(Duration::parse("5y").is_err());
456
457        // Long forms not supported
458        assert!(Duration::parse("5minutes").is_err());
459        assert!(Duration::parse("10seconds").is_err());
460        assert!(Duration::parse("2hours").is_err());
461        assert!(Duration::parse("500millis").is_err());
462        assert!(Duration::parse("30sec").is_err());
463        assert!(Duration::parse("5min").is_err());
464        assert!(Duration::parse("2hr").is_err());
465
466        // No suffix
467        assert!(Duration::parse("60").is_err());
468        assert!(Duration::parse("0").is_err());
469        assert!(Duration::parse("3600").is_err());
470        assert!(Duration::parse("1").is_err());
471
472        // Empty or whitespace only
473        assert!(Duration::parse("").is_err());
474        assert!(Duration::parse("   ").is_err());
475
476        // Negative numbers (should fail because we use u64)
477        assert!(Duration::parse("-5s").is_err());
478        assert!(Duration::parse("-100ms").is_err());
479
480        // Mixed case should not work (we only support lowercase)
481        assert!(Duration::parse("30S").is_err());
482        assert!(Duration::parse("500MS").is_err());
483        assert!(Duration::parse("2H").is_err());
484    }
485
486    #[test]
487    fn test_duration_parse_large_values() {
488        // Large values that don't overflow
489        assert_eq!(
490            Duration::parse("999999999ms").unwrap(),
491            Duration::from_millis(999999999)
492        );
493        assert_eq!(
494            Duration::parse("99999999s").unwrap(),
495            Duration::from_secs(99999999)
496        );
497    }
498
499    #[test]
500    fn test_duration_parse_overflow_cases() {
501        // Test hours overflow
502        let max_safe_hours = u64::MAX / 3600;
503        let overflow_hours = max_safe_hours + 1;
504        assert!(Duration::parse(&format!("{max_safe_hours}h")).is_ok());
505        match Duration::parse(&format!("{overflow_hours}h")) {
506            Err(msg) => assert!(msg.contains("too large (would overflow)")),
507            Ok(_) => panic!("Expected overflow error for large hours value"),
508        }
509        match Duration::parse(&format!("{}h", u64::MAX)) {
510            Err(msg) => assert!(msg.contains("too large (would overflow)")),
511            Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
512        }
513
514        // Test minutes overflow
515        let max_safe_minutes = u64::MAX / 60;
516        let overflow_minutes = max_safe_minutes + 1;
517        assert!(Duration::parse(&format!("{max_safe_minutes}m")).is_ok());
518        match Duration::parse(&format!("{overflow_minutes}m")) {
519            Err(msg) => assert!(msg.contains("too large (would overflow)")),
520            Ok(_) => panic!("Expected overflow error for large minutes value"),
521        }
522        match Duration::parse(&format!("{}m", u64::MAX)) {
523            Err(msg) => assert!(msg.contains("too large (would overflow)")),
524            Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
525        }
526    }
527}