human_units/
duration.rs

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