1use rand::Rng;
4use std::time::{Duration, SystemTime};
5
6pub fn parse_duration(s: &str) -> Result<Duration, String> {
56 let s = s.trim();
57
58 if let Some(num_str) = s.strip_suffix("ms") {
60 let millis: u64 = num_str
61 .trim()
62 .parse()
63 .map_err(|_| format!("Invalid milliseconds value: '{num_str}'"))?;
64 return Ok(Duration::from_millis(millis));
65 }
66
67 if let Some(num_str) = s.strip_suffix("h") {
69 let hours: u64 = num_str
70 .trim()
71 .parse()
72 .map_err(|_| format!("Invalid hours value: '{num_str}'"))?;
73 let seconds = hours
74 .checked_mul(3600)
75 .ok_or_else(|| format!("Hours value too large (would overflow): '{hours}'"))?;
76 return Ok(Duration::from_secs(seconds));
77 }
78
79 if let Some(num_str) = s.strip_suffix("m") {
81 let minutes: u64 = num_str
82 .trim()
83 .parse()
84 .map_err(|_| format!("Invalid minutes value: '{num_str}'"))?;
85 let seconds = minutes
86 .checked_mul(60)
87 .ok_or_else(|| format!("Minutes value too large (would overflow): '{minutes}'"))?;
88 return Ok(Duration::from_secs(seconds));
89 }
90
91 if let Some(num_str) = s.strip_suffix("s") {
93 let secs: u64 = num_str
94 .trim()
95 .parse()
96 .map_err(|_| format!("Invalid seconds value: '{num_str}'"))?;
97 return Ok(Duration::from_secs(secs));
98 }
99
100 Err(format!(
102 "Invalid duration format: '{s}'. A suffix is required. \
103 Supported formats: '123ms', '30s', '5m', '2h'"
104 ))
105}
106
107pub trait SystemTimeExt {
109 fn epoch(&self) -> Duration;
113
114 fn epoch_millis(&self) -> u64;
119
120 fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime;
123}
124
125impl SystemTimeExt for SystemTime {
126 fn epoch(&self) -> Duration {
127 self.duration_since(std::time::UNIX_EPOCH)
128 .expect("failed to get epoch time")
129 }
130
131 fn epoch_millis(&self) -> u64 {
132 self.epoch().as_millis().min(u64::MAX as u128) as u64
133 }
134
135 fn add_jittered(&self, rng: &mut impl Rng, jitter: Duration) -> SystemTime {
136 *self + rng.gen_range(Duration::default()..=jitter * 2)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_epoch() {
146 let time = SystemTime::UNIX_EPOCH;
147 assert_eq!(time.epoch(), Duration::from_secs(0));
148
149 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
150 assert_eq!(time.epoch(), Duration::from_millis(1_001));
151 }
152
153 #[test]
154 #[should_panic(expected = "failed to get epoch time")]
155 fn test_epoch_panics() {
156 let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
157 time.epoch();
158 }
159
160 #[test]
161 fn test_epoch_millis() {
162 let time = SystemTime::UNIX_EPOCH;
163 assert_eq!(time.epoch_millis(), 0);
164
165 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_millis(1);
166 assert_eq!(time.epoch_millis(), 1_001);
167
168 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1) + Duration::from_nanos(999_999);
170 assert_eq!(time.epoch_millis(), 1_000);
171
172 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(300);
174 assert_eq!(time.epoch_millis(), 300_000);
175 }
176
177 #[test]
178 #[should_panic(expected = "failed to get epoch time")]
179 fn test_epoch_millis_panics() {
180 let time = SystemTime::UNIX_EPOCH - Duration::from_secs(1);
181 time.epoch_millis();
182 }
183
184 #[test]
185 fn test_add_jittered() {
186 let mut rng = rand::thread_rng();
187 let time = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
188 let jitter = Duration::from_secs(2);
189
190 let (mut below, mut above) = (false, false);
192 let avg = time + jitter;
193 for _ in 0..100 {
194 let new_time = time.add_jittered(&mut rng, jitter);
195
196 below |= new_time < avg;
198 above |= new_time > avg;
199
200 assert!(new_time >= time);
202 assert!(new_time <= time + (jitter * 2));
203 }
204 assert!(below && above);
205 }
206
207 #[test]
208 fn test_parse_duration_milliseconds() {
209 assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500));
210 assert_eq!(parse_duration("0ms").unwrap(), Duration::from_millis(0));
211 assert_eq!(parse_duration("1ms").unwrap(), Duration::from_millis(1));
212 assert_eq!(
213 parse_duration("1000ms").unwrap(),
214 Duration::from_millis(1000)
215 );
216 assert_eq!(parse_duration("250ms").unwrap(), Duration::from_millis(250));
217 }
218
219 #[test]
220 fn test_parse_duration_seconds() {
221 assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
222 assert_eq!(parse_duration("0s").unwrap(), Duration::from_secs(0));
223 assert_eq!(parse_duration("1s").unwrap(), Duration::from_secs(1));
224 assert_eq!(parse_duration("45s").unwrap(), Duration::from_secs(45));
225 assert_eq!(parse_duration("60s").unwrap(), Duration::from_secs(60));
226 assert_eq!(parse_duration("3600s").unwrap(), Duration::from_secs(3600));
227 }
228
229 #[test]
230 fn test_parse_duration_minutes() {
231 assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
232 assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
233 assert_eq!(parse_duration("0m").unwrap(), Duration::from_secs(0));
234 assert_eq!(parse_duration("10m").unwrap(), Duration::from_secs(600));
235 assert_eq!(parse_duration("15m").unwrap(), Duration::from_secs(900));
236 assert_eq!(parse_duration("30m").unwrap(), Duration::from_secs(1800));
237 assert_eq!(parse_duration("60m").unwrap(), Duration::from_secs(3600));
238 }
239
240 #[test]
241 fn test_parse_duration_hours() {
242 assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
243 assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
244 assert_eq!(parse_duration("0h").unwrap(), Duration::from_secs(0));
245 assert_eq!(parse_duration("3h").unwrap(), Duration::from_secs(10800));
246 assert_eq!(parse_duration("4h").unwrap(), Duration::from_secs(14400));
247 assert_eq!(parse_duration("12h").unwrap(), Duration::from_secs(43200));
248 assert_eq!(parse_duration("24h").unwrap(), Duration::from_secs(86400));
249 assert_eq!(parse_duration("168h").unwrap(), Duration::from_secs(604800));
250 }
252
253 #[test]
254 fn test_parse_duration_whitespace() {
255 assert_eq!(parse_duration(" 30s ").unwrap(), Duration::from_secs(30));
257 assert_eq!(
258 parse_duration("\t500ms\n").unwrap(),
259 Duration::from_millis(500)
260 );
261 assert_eq!(parse_duration(" 2h ").unwrap(), Duration::from_secs(7200));
262
263 assert_eq!(parse_duration("30 s").unwrap(), Duration::from_secs(30));
265 assert_eq!(
266 parse_duration("500 ms").unwrap(),
267 Duration::from_millis(500)
268 );
269 assert_eq!(parse_duration("2 h").unwrap(), Duration::from_secs(7200));
270 assert_eq!(parse_duration("5 m").unwrap(), Duration::from_secs(300));
271 }
272
273 #[test]
274 fn test_parse_duration_error_cases() {
275 assert!(parse_duration("invalid").is_err());
277 assert!(parse_duration("abc123ms").is_err());
278 assert!(parse_duration("12.5s").is_err()); assert!(parse_duration("10x").is_err());
282 assert!(parse_duration("30days").is_err());
283 assert!(parse_duration("5y").is_err());
284
285 assert!(parse_duration("5minutes").is_err());
287 assert!(parse_duration("10seconds").is_err());
288 assert!(parse_duration("2hours").is_err());
289 assert!(parse_duration("500millis").is_err());
290 assert!(parse_duration("30sec").is_err());
291 assert!(parse_duration("5min").is_err());
292 assert!(parse_duration("2hr").is_err());
293
294 assert!(parse_duration("60").is_err());
296 assert!(parse_duration("0").is_err());
297 assert!(parse_duration("3600").is_err());
298 assert!(parse_duration("1").is_err());
299
300 assert!(parse_duration("").is_err());
302 assert!(parse_duration(" ").is_err());
303
304 assert!(parse_duration("-5s").is_err());
306 assert!(parse_duration("-100ms").is_err());
307
308 assert!(parse_duration("30S").is_err());
310 assert!(parse_duration("500MS").is_err());
311 assert!(parse_duration("2H").is_err());
312 }
313
314 #[test]
315 fn test_parse_duration_large_values() {
316 assert_eq!(
318 parse_duration("999999999ms").unwrap(),
319 Duration::from_millis(999999999)
320 );
321 assert_eq!(
322 parse_duration("99999999s").unwrap(),
323 Duration::from_secs(99999999)
324 );
325 }
326
327 #[test]
328 fn test_parse_duration_overflow_cases() {
329 let max_safe_hours = u64::MAX / 3600;
331 let overflow_hours = max_safe_hours + 1;
332 assert!(parse_duration(&format!("{max_safe_hours}h")).is_ok());
333 match parse_duration(&format!("{overflow_hours}h")) {
334 Err(msg) => assert!(msg.contains("too large (would overflow)")),
335 Ok(_) => panic!("Expected overflow error for large hours value"),
336 }
337 match parse_duration(&format!("{}h", u64::MAX)) {
338 Err(msg) => assert!(msg.contains("too large (would overflow)")),
339 Ok(_) => panic!("Expected overflow error for u64::MAX hours"),
340 }
341
342 let max_safe_minutes = u64::MAX / 60;
344 let overflow_minutes = max_safe_minutes + 1;
345 assert!(parse_duration(&format!("{max_safe_minutes}m")).is_ok());
346 match parse_duration(&format!("{overflow_minutes}m")) {
347 Err(msg) => assert!(msg.contains("too large (would overflow)")),
348 Ok(_) => panic!("Expected overflow error for large minutes value"),
349 }
350 match parse_duration(&format!("{}m", u64::MAX)) {
351 Err(msg) => assert!(msg.contains("too large (would overflow)")),
352 Ok(_) => panic!("Expected overflow error for u64::MAX minutes"),
353 }
354 }
355}