roundable/
duration.rs

1//! Functions, constants, etc. related to Duration.
2
3use crate::{Roundable, Tie};
4use core::time::Duration;
5
6/// A microsecond. Useful for rounding [`Duration`].
7///
8/// ```rust
9/// use roundable::{MICROSECOND, Roundable, Tie};
10/// use std::time::Duration;
11///
12/// assert!(MICROSECOND == Duration::from_nanos(500).round_to(MICROSECOND, Tie::Up));
13/// ```
14pub const MICROSECOND: Duration = Duration::from_micros(1);
15
16/// A millisecond. Useful for rounding [`Duration`].
17///
18/// ```rust
19/// use roundable::{MILLISECOND, Roundable, Tie};
20/// use std::time::Duration;
21///
22/// assert!(MILLISECOND == Duration::from_micros(500).round_to(MILLISECOND, Tie::Up));
23/// ```
24pub const MILLISECOND: Duration = Duration::from_millis(1);
25
26/// A second. Useful for rounding [`Duration`].
27///
28/// ```rust
29/// use roundable::{SECOND, Roundable, Tie};
30/// use std::time::Duration;
31///
32/// assert!(SECOND == Duration::from_millis(500).round_to(SECOND, Tie::Up));
33/// ```
34pub const SECOND: Duration = Duration::from_secs(1);
35
36/// A minute. Useful for rounding [`Duration`].
37///
38/// ```rust
39/// use roundable::{MINUTE, Roundable, Tie};
40/// use std::time::Duration;
41///
42/// assert!(MINUTE == Duration::from_secs(30).round_to(MINUTE, Tie::Up));
43/// ```
44pub const MINUTE: Duration = Duration::from_secs(60);
45
46/// An hour. Useful for rounding [`Duration`].
47///
48/// ```rust
49/// use roundable::{HOUR, Roundable, Tie};
50/// use std::time::Duration;
51///
52/// assert!(HOUR == Duration::from_secs(30*60).round_to(HOUR, Tie::Up));
53/// ```
54pub const HOUR: Duration = Duration::from_secs(60 * 60);
55
56impl Roundable for Duration {
57    fn try_round_to(self, factor: Self, tie: Tie) -> Option<Self> {
58        // Duration will always fit into u128 as nanoseconds.
59        self.as_nanos()
60            .try_round_to(factor.as_nanos(), tie)
61            .map(nanos_to_duration)
62    }
63}
64
65/// Create a new [`Duration`] from a `u128` of nanoseconds.
66///
67/// This is essentially just [`Duration::from_nanos()`] but it works on a
68/// `u128`, which can represent any valid `Duration`.
69///
70/// # Panics
71///
72/// `Duration` can only represent 64 bits worth of seconds and less than 32 bits
73/// (1e9) worth of nanoseconds, which works out to being roughly 94 bits. A
74/// `u128` can therefore represent values that are invalid `Duration`s. This
75/// will panic in those cases.
76///
77/// `nanos_to_duration(duration.as_nanos())` should never panic.
78///
79/// ```rust
80/// use roundable::nanos_to_duration;
81/// use std::time::Duration;
82///
83/// assert!(Duration::MAX == nanos_to_duration(Duration::MAX.as_nanos()));
84/// assert!(Duration::ZERO == nanos_to_duration(Duration::ZERO.as_nanos()));
85/// ```
86#[must_use]
87pub fn nanos_to_duration(total: u128) -> Duration {
88    /// Just to make things clear.
89    const NANOS_PER_SECOND: u128 = 1_000_000_000;
90    #[allow(clippy::integer_division)]
91    Duration::new(
92        (total / NANOS_PER_SECOND).try_into().expect(
93            "nanos_to_duration() overflowed seconds value for Duration",
94        ),
95        (total % NANOS_PER_SECOND).try_into().unwrap(),
96    )
97}
98
99#[cfg(test)]
100#[allow(clippy::cognitive_complexity)]
101mod tests {
102    use super::*;
103    use assert2::check;
104
105    /// Convenient alias for [`Duration::from_millis()`].
106    const fn ms(ms: u64) -> Duration {
107        Duration::from_millis(ms)
108    }
109
110    #[test]
111    fn round_millisecond_to_nearest_millisecond() {
112        check!(ms(10) == ms(10).round_to(MILLISECOND, Tie::Up));
113
114        check!(ms(10) == ms(10).round_to(ms(2), Tie::Up));
115        check!(ms(10) == ms(9).round_to(ms(2), Tie::Up));
116
117        check!(ms(9) == ms(9).round_to(ms(3), Tie::Up));
118        check!(ms(9) == ms(10).round_to(ms(3), Tie::Up));
119        check!(ms(12) == ms(11).round_to(ms(3), Tie::Up));
120        check!(ms(12) == ms(12).round_to(ms(3), Tie::Up));
121    }
122
123    #[test]
124    fn round_second_to_nearest_millisecond() {
125        check!(ms(1_010) == ms(1_010).round_to(MILLISECOND, Tie::Up));
126
127        check!(ms(1_010) == ms(1_010).round_to(ms(2), Tie::Up));
128        check!(ms(1_010) == ms(1_009).round_to(ms(2), Tie::Up));
129
130        check!(ms(1_008) == ms(1_008).round_to(ms(3), Tie::Up));
131        check!(ms(1_008) == ms(1_009).round_to(ms(3), Tie::Up));
132        check!(ms(1_011) == ms(1_010).round_to(ms(3), Tie::Up));
133        check!(ms(1_011) == ms(1_011).round_to(ms(3), Tie::Up));
134    }
135
136    #[test]
137    fn round_second_to_nearest_second() {
138        check!(ms(0) == ms(499).round_to(SECOND, Tie::Up));
139        check!(SECOND == ms(500).round_to(SECOND, Tie::Up));
140        check!(SECOND == ms(1_010).round_to(SECOND, Tie::Up));
141        check!(SECOND == ms(1_499).round_to(SECOND, Tie::Up));
142        check!(ms(2_000) == ms(1_500).round_to(SECOND, Tie::Up));
143
144        check!(ms(1_001) == ms(1_000).round_to(ms(1_001), Tie::Up));
145        check!(ms(1_001) == ms(1_001).round_to(ms(1_001), Tie::Up));
146        check!(ms(1_001) == ms(1_002).round_to(ms(1_001), Tie::Up));
147    }
148
149    #[test]
150    fn round_to_giant_factor() {
151        check!(ms(0) == ms(1_000_000).round_to(Duration::MAX, Tie::Up));
152        check!(Duration::MAX == Duration::MAX.round_to(Duration::MAX, Tie::Up));
153    }
154
155    #[test]
156    #[should_panic(expected = "try_round_to() requires positive factor")]
157    fn round_to_zero_factor() {
158        let _ = ms(10).round_to(ms(0), Tie::Up);
159    }
160
161    /// Theoretical maximum Duration as nanoseconds (based on u64 for seconds).
162    const NANOS_MAX: u128 = u64::MAX as u128 * 1_000_000_000 + 999_999_999;
163
164    #[test]
165    #[allow(clippy::arithmetic_side_effects)]
166    fn nanos_to_duration_ok() {
167        check!(Duration::ZERO == nanos_to_duration(0));
168        check!(Duration::new(1, 1) == nanos_to_duration(1_000_000_001));
169
170        // Check Duration::MAX two ways, since according it its docs it can vary
171        // based on platform.
172        check!(Duration::MAX == nanos_to_duration(Duration::MAX.as_nanos()));
173        check!(
174            Duration::new(u64::MAX, 999_999_999)
175                == nanos_to_duration(NANOS_MAX)
176        );
177    }
178
179    #[test]
180    #[should_panic(
181        expected = "nanos_to_duration() overflowed seconds value for Duration: TryFromIntError(())"
182    )]
183    fn nanos_to_duration_overflow() {
184        let _ = nanos_to_duration(Duration::MAX.as_nanos() + 1);
185    }
186
187    #[test]
188    #[should_panic(
189        expected = "nanos_to_duration() overflowed seconds value for Duration: TryFromIntError(())"
190    )]
191    #[allow(clippy::arithmetic_side_effects)]
192    fn nanos_to_duration_overflow_manual() {
193        // One over the maximum duration. Just in case `Duration::MAX` is some
194        // other value, since the docs say it can vary by platform even if it
195        // currently is always `u64::MAX * 1_000_000_000 + 999_999_999`.
196        let _ = nanos_to_duration(NANOS_MAX + 1);
197    }
198}