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