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#[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 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#[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}