human_units/
duration.rs

1use crate::u128_is_multiple_of;
2use core::fmt::Debug;
3use core::fmt::Display;
4use core::num::NonZeroU128;
5use core::num::NonZeroU16;
6use core::ops::Deref;
7use core::ops::DerefMut;
8use core::str::FromStr;
9use core::time::Duration as StdDuration;
10
11/**
12Exact duration.
13
14The intended use is the configuration files where exact values are required,
15i.e. timeouts, cache max age, time-to-live etc.
16*/
17#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
18#[cfg_attr(all(test, feature = "std"), derive(arbitrary::Arbitrary))]
19#[repr(transparent)]
20pub struct Duration(pub StdDuration);
21
22impl Duration {
23    /// Max. length of the duration in string form.
24    pub const MAX_STRING_LEN: usize = 31;
25}
26
27impl Display for Duration {
28    #[allow(clippy::assign_op_pattern)]
29    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
30        let mut duration = self.0.as_nanos();
31        let unit = if duration == 0 {
32            "s"
33        } else {
34            let mut unit = "ns";
35            for u in UNITS {
36                let d: NonZeroU128 = u.0.into();
37                if !u128_is_multiple_of(duration, d.into()) {
38                    break;
39                }
40                duration = duration / d;
41                unit = u.1;
42            }
43            unit
44        };
45        write!(f, "{duration}{unit}")
46    }
47}
48
49impl FromStr for Duration {
50    type Err = DurationError;
51    fn from_str(other: &str) -> Result<Self, Self::Err> {
52        let other = other.trim();
53        match other.rfind(char::is_numeric) {
54            None => Err(DurationError),
55            Some(i) => {
56                let duration: u128 = other[..=i].parse().map_err(|_| DurationError)?;
57                let unit = other[(i + 1)..].trim();
58                let factor = unit_to_factor(unit)? as u128;
59                let duration = duration.checked_mul(factor).ok_or(DurationError)?;
60                let seconds: u64 = (duration / NANOS_PER_SEC as u128)
61                    .try_into()
62                    .map_err(|_| DurationError)?;
63                let nanoseconds = (duration % NANOS_PER_SEC as u128) as u32;
64                Ok(Self(StdDuration::new(seconds, nanoseconds)))
65            }
66        }
67    }
68}
69
70impl From<StdDuration> for Duration {
71    fn from(other: StdDuration) -> Self {
72        Self(other)
73    }
74}
75
76impl From<Duration> for StdDuration {
77    fn from(other: Duration) -> Self {
78        other.0
79    }
80}
81
82impl Deref for Duration {
83    type Target = StdDuration;
84
85    fn deref(&self) -> &Self::Target {
86        &self.0
87    }
88}
89
90impl DerefMut for Duration {
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        &mut self.0
93    }
94}
95
96fn unit_to_factor(unit: &str) -> Result<u64, DurationError> {
97    match unit {
98        "ns" => Ok(1_u64),
99        "μs" => Ok(1000_u64),
100        "ms" => Ok(1000_u64 * 1000_u64),
101        "s" | "" => Ok(1000_u64 * 1000_u64 * 1000_u64),
102        "m" => Ok(60_u64 * 1000_u64 * 1000_u64 * 1000_u64),
103        "h" => Ok(60_u64 * 60_u64 * 1000_u64 * 1000_u64 * 1000_u64),
104        "d" => Ok(24_u64 * 60_u64 * 60_u64 * 1000_u64 * 1000_u64 * 1000_u64),
105        _ => Err(DurationError),
106    }
107}
108
109/// Duration parsing error.
110#[derive(Debug)]
111pub struct DurationError;
112
113impl Display for DurationError {
114    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
115        Debug::fmt(self, f)
116    }
117}
118
119#[cfg(feature = "std")]
120impl std::error::Error for DurationError {}
121
122const UNITS: [(NonZeroU16, &str); 6] = [
123    (unsafe { NonZeroU16::new_unchecked(1000) }, "μs"),
124    (unsafe { NonZeroU16::new_unchecked(1000) }, "ms"),
125    (unsafe { NonZeroU16::new_unchecked(1000) }, "s"),
126    (unsafe { NonZeroU16::new_unchecked(60) }, "m"),
127    (unsafe { NonZeroU16::new_unchecked(60) }, "h"),
128    (unsafe { NonZeroU16::new_unchecked(24) }, "d"),
129];
130
131const NANOS_PER_SEC: u32 = 1_000_000_000_u32;
132
133#[cfg(all(test, feature = "std"))]
134mod tests {
135
136    use std::ops::AddAssign;
137
138    use arbtest::arbtest;
139
140    use super::*;
141
142    #[test]
143    fn test_duration_display() {
144        assert_eq!("123s", Duration(StdDuration::from_secs(123)).to_string());
145        assert_eq!("2m", Duration(StdDuration::from_secs(120)).to_string());
146        assert_eq!(
147            "1d",
148            Duration(StdDuration::from_secs(24 * 60 * 60)).to_string()
149        );
150        assert_eq!(
151            "23h",
152            Duration(StdDuration::from_secs(23 * 60 * 60)).to_string()
153        );
154        assert_eq!("0s", Duration(StdDuration::from_secs(0)).to_string());
155        assert_eq!("1μs", Duration(StdDuration::from_nanos(1000)).to_string());
156    }
157
158    #[test]
159    fn test_duration_parse() {
160        assert_eq!(Duration(StdDuration::from_secs(1)), "1".parse().unwrap());
161        assert_eq!(
162            Duration(StdDuration::from_nanos(1000)),
163            "1μs".parse().unwrap()
164        );
165        assert_eq!(Duration(StdDuration::from_secs(120)), "2m".parse().unwrap());
166        assert_eq!(
167            "Err(DurationError)",
168            format!("{:?}", "2km".parse::<Duration>())
169        );
170        assert_eq!(
171            "Err(DurationError)",
172            format!("{:?}", "ms".parse::<Duration>())
173        );
174        assert_eq!(
175            "Err(DurationError)",
176            format!("{:?}", format!("{}0", u128::MAX).parse::<Duration>())
177        );
178    }
179
180    #[test]
181    fn test_deref() {
182        assert_eq!(
183            StdDuration::from_secs(1),
184            *Duration(StdDuration::from_secs(1)),
185        );
186        let mut tmp = Duration(StdDuration::from_secs(1));
187        tmp.add_assign(StdDuration::from_secs(1));
188        assert_eq!(StdDuration::from_secs(2), *tmp);
189    }
190
191    #[test]
192    fn test_from_into() {
193        let d1 = Duration(StdDuration::from_secs(1));
194        let d2: StdDuration = d1.into();
195        let d3: Duration = d2.into();
196        assert_eq!(d1, d3);
197        assert_eq!(d1.0, d2);
198    }
199
200    #[test]
201    fn from_str_overflow_does_not_panic() {
202        let expected = u64::MAX;
203        let string = format!("{expected}d");
204        assert!(string.parse::<Duration>().is_err(), "string = {string:?}");
205        let expected = u128::MAX;
206        let string = format!("{expected}d");
207        assert!(string.parse::<Duration>().is_err(), "string = {string:?}");
208    }
209
210    #[test]
211    fn display_parse_symmetry() {
212        arbtest(|u| {
213            let expected: Duration = u.arbitrary()?;
214            let string = expected.to_string();
215            let actual: Duration = string.parse().unwrap();
216            assert_eq!(expected, actual, "string = `{string}`");
217            Ok(())
218        });
219    }
220
221    #[test]
222    fn parse_display_symmetry() {
223        arbtest(|u| {
224            let (unit, max) = *u
225                .choose(&[
226                    ("ns", MAX_NANOSECONDS),
227                    ("μs", MAX_NANOSECONDS / 1000_u128),
228                    ("ms", MAX_NANOSECONDS / (1000_u128 * 1000_u128)),
229                    ("s", MAX_NANOSECONDS / (1000_u128 * 1000_u128 * 1000_u128)),
230                    (
231                        "m",
232                        MAX_NANOSECONDS / (1000_u128 * 1000_u128 * 1000_u128 * 60_u128),
233                    ),
234                    (
235                        "h",
236                        MAX_NANOSECONDS / (1000_u128 * 1000_u128 * 1000_u128 * 60_u128 * 60_u128),
237                    ),
238                    (
239                        "d",
240                        MAX_NANOSECONDS
241                            / (1000_u128 * 1000_u128 * 1000_u128 * 60_u128 * 60_u128 * 24_u128),
242                    ),
243                ])
244                .unwrap();
245            let number: u128 = u.int_in_range(0_u128..=max)?;
246            let prefix = *u.choose(&["", " ", "  "]).unwrap();
247            let infix = *u.choose(&["", " ", "  "]).unwrap();
248            let suffix = *u.choose(&["", " ", "  "]).unwrap();
249            let expected = format!("{prefix}{number}{infix}{unit}{suffix}");
250            let expected_duration: Duration = expected.parse().unwrap();
251            let actual = expected_duration.to_string();
252            let actual_duration: Duration = actual.parse().unwrap();
253            assert_eq!(
254                expected_duration, actual_duration,
255                "string 1 = `{expected}`, string 2 = `{actual}`"
256            );
257            assert!(
258                expected == actual
259                    || u128_is_multiple_of(actual_duration.0.as_nanos(), number)
260                    || number == 0
261            );
262            Ok(())
263        });
264    }
265
266    const MAX_NANOSECONDS: u128 =
267        (u64::MAX as u128) * (NANOS_PER_SEC as u128) + (NANOS_PER_SEC as u128) - 1_u128;
268}