reliakit_primitives/
duration.rs1use crate::{PrimitiveError, PrimitiveResult};
2use alloc::string::String;
3use core::{fmt, str::FromStr, time::Duration};
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct HumanDuration(Duration);
12
13impl HumanDuration {
14 pub fn parse(s: &str) -> PrimitiveResult<Self> {
24 if s.is_empty() {
25 return Err(PrimitiveError::Empty);
26 }
27
28 const RANK_H: u8 = 4;
31 const RANK_M: u8 = 3;
32 const RANK_S: u8 = 2;
33 const RANK_MS: u8 = 1;
34
35 let mut total_nanos: u128 = 0;
36 let mut last_rank: u8 = u8::MAX;
37 let mut found_any = false;
38 let mut pos = 0;
39 let bytes = s.as_bytes();
40
41 while pos < bytes.len() {
42 let num_start = pos;
44 while pos < bytes.len() && bytes[pos].is_ascii_digit() {
45 pos += 1;
46 }
47 if pos == num_start {
48 return Err(PrimitiveError::Invalid {
49 message: "expected a number before unit",
50 });
51 }
52 let num_str = &s[num_start..pos];
53 let num = parse_u64(num_str).ok_or(PrimitiveError::Invalid {
54 message: "duration number is too large",
55 })?;
56
57 let unit_start = pos;
59 while pos < bytes.len() && bytes[pos].is_ascii_alphabetic() {
60 pos += 1;
61 }
62 let unit = &s[unit_start..pos];
63
64 let (nanos_per_unit, rank): (u128, u8) = match unit {
65 "h" => (3_600 * 1_000_000_000, RANK_H),
66 "m" => (60 * 1_000_000_000, RANK_M),
67 "s" => (1_000_000_000, RANK_S),
68 "ms" => (1_000_000, RANK_MS),
69 _ => {
70 return Err(PrimitiveError::Invalid {
71 message: "unknown time unit; use h, m, s, or ms",
72 })
73 }
74 };
75
76 if rank >= last_rank {
77 return Err(PrimitiveError::Invalid {
78 message: "units must be in descending order (h, m, s, ms) with no duplicates",
79 });
80 }
81 last_rank = rank;
82
83 let component =
84 (num as u128)
85 .checked_mul(nanos_per_unit)
86 .ok_or(PrimitiveError::Invalid {
87 message: "duration overflow",
88 })?;
89
90 total_nanos = total_nanos
91 .checked_add(component)
92 .ok_or(PrimitiveError::Invalid {
93 message: "duration overflow",
94 })?;
95
96 found_any = true;
97 }
98
99 if !found_any {
100 return Err(PrimitiveError::Invalid {
101 message: "no duration components found",
102 });
103 }
104
105 let secs = (total_nanos / 1_000_000_000) as u64;
106 let nanos = (total_nanos % 1_000_000_000) as u32;
107 Ok(Self(Duration::new(secs, nanos)))
108 }
109
110 pub fn as_duration(self) -> Duration {
112 self.0
113 }
114
115 pub fn as_secs(self) -> u64 {
117 self.0.as_secs()
118 }
119
120 pub fn as_millis(self) -> u128 {
122 self.0.as_millis()
123 }
124}
125
126fn parse_u64(s: &str) -> Option<u64> {
127 if s.is_empty() {
128 return None;
129 }
130 let mut result: u64 = 0;
131 for c in s.chars() {
132 let digit = c.to_digit(10)? as u64;
133 result = result.checked_mul(10)?.checked_add(digit)?;
134 }
135 Some(result)
136}
137
138impl fmt::Display for HumanDuration {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 let total_secs = self.0.as_secs();
141 let millis = self.0.subsec_millis();
142 let h = total_secs / 3600;
143 let m = (total_secs % 3600) / 60;
144 let s = total_secs % 60;
145
146 let mut wrote = false;
147 if h > 0 {
148 write!(f, "{h}h")?;
149 wrote = true;
150 }
151 if m > 0 {
152 write!(f, "{m}m")?;
153 wrote = true;
154 }
155 if s > 0 || millis > 0 {
156 if s > 0 {
157 write!(f, "{s}s")?;
158 }
159 if millis > 0 {
160 write!(f, "{millis}ms")?;
161 }
162 wrote = true;
163 }
164 if !wrote {
165 write!(f, "0s")?;
166 }
167 Ok(())
168 }
169}
170
171impl FromStr for HumanDuration {
172 type Err = PrimitiveError;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 Self::parse(s)
176 }
177}
178
179impl PartialEq<str> for HumanDuration {
180 fn eq(&self, other: &str) -> bool {
181 Self::parse(other).is_ok_and(|other| self == &other)
182 }
183}
184
185impl PartialEq<&str> for HumanDuration {
186 fn eq(&self, other: &&str) -> bool {
187 self.eq(*other)
188 }
189}
190
191impl PartialEq<String> for HumanDuration {
192 fn eq(&self, other: &String) -> bool {
193 self.eq(other.as_str())
194 }
195}
196
197impl PartialEq<&String> for HumanDuration {
198 fn eq(&self, other: &&String) -> bool {
199 self.eq(other.as_str())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::HumanDuration;
206 use crate::PrimitiveError;
207 use alloc::string::ToString;
208
209 #[test]
210 fn parses_seconds() {
211 assert_eq!(HumanDuration::parse("45s").unwrap().as_secs(), 45);
212 }
213
214 #[test]
215 fn parses_minutes() {
216 assert_eq!(HumanDuration::parse("2m").unwrap().as_secs(), 120);
217 }
218
219 #[test]
220 fn parses_hours() {
221 assert_eq!(HumanDuration::parse("1h").unwrap().as_secs(), 3600);
222 }
223
224 #[test]
225 fn parses_milliseconds() {
226 assert_eq!(HumanDuration::parse("500ms").unwrap().as_millis(), 500);
227 }
228
229 #[test]
230 fn parses_combination() {
231 let d = HumanDuration::parse("1h30m45s").unwrap();
232 assert_eq!(d.as_secs(), 3600 + 1800 + 45);
233 }
234
235 #[test]
236 fn parses_minutes_and_seconds() {
237 assert_eq!(HumanDuration::parse("2m30s").unwrap().as_secs(), 150);
238 }
239
240 #[test]
241 fn rejects_empty() {
242 assert_eq!(HumanDuration::parse("").unwrap_err(), PrimitiveError::Empty);
243 }
244
245 #[test]
246 fn rejects_unknown_unit() {
247 assert!(HumanDuration::parse("5d").is_err());
248 }
249
250 #[test]
251 fn rejects_no_number() {
252 assert!(HumanDuration::parse("s").is_err());
253 }
254
255 #[test]
256 fn rejects_out_of_order_units() {
257 assert!(HumanDuration::parse("1s1h").is_err());
258 }
259
260 #[test]
261 fn rejects_duplicate_units() {
262 assert!(HumanDuration::parse("1h1h").is_err());
263 }
264
265 #[test]
266 fn rejects_ms_before_s() {
267 assert!(HumanDuration::parse("500ms30s").is_err());
268 }
269
270 #[test]
271 fn as_duration() {
272 let d = HumanDuration::parse("1s").unwrap();
273 assert_eq!(d.as_duration().as_secs(), 1);
274 }
275
276 #[test]
277 fn display_seconds() {
278 assert_eq!(HumanDuration::parse("45s").unwrap().to_string(), "45s");
279 }
280
281 #[test]
282 fn display_combined() {
283 assert_eq!(HumanDuration::parse("1h30m").unwrap().to_string(), "1h30m");
284 }
285
286 #[test]
287 fn display_zero() {
288 assert_eq!(HumanDuration::parse("0s").unwrap().to_string(), "0s");
289 }
290
291 #[test]
292 fn display_mixed_seconds_and_millis() {
293 assert_eq!(
294 HumanDuration::parse("1s500ms").unwrap().to_string(),
295 "1s500ms"
296 );
297 }
298
299 #[test]
300 fn display_millis_only() {
301 assert_eq!(HumanDuration::parse("500ms").unwrap().to_string(), "500ms");
302 }
303
304 #[test]
305 fn from_str_and_string_comparisons() {
306 let duration = "1m30s".parse::<HumanDuration>().unwrap();
307 let owned = "90s".to_string();
308 assert_eq!(duration, "1m30s");
309 assert_eq!(duration, owned);
310 assert!("1s1m".parse::<HumanDuration>().is_err());
311 }
312}