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