1use std::error::Error as StdError;
2use std::fmt;
3use std::str::Chars;
4use std::time::Duration;
5
6#[derive(Debug, PartialEq, Clone)]
8pub enum Error {
9 InvalidCharacter(usize),
15 NumberExpected(usize),
24 UnknownUnit {
32 start: usize,
34 end: usize,
36 unit: String,
38 value: u64,
40 },
41 NumberOverflow,
47 Empty,
49}
50
51impl StdError for Error {}
52
53impl fmt::Display for Error {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 match self {
56 Error::InvalidCharacter(offset) => write!(f, "invalid character at {}", offset),
57 Error::NumberExpected(offset) => write!(f, "expected number at {}", offset),
58 Error::UnknownUnit { unit, value, .. } if unit.is_empty() => {
59 write!(f, "time unit needed, for example {0}sec or {0}ms", value,)
60 }
61 Error::UnknownUnit { unit, .. } => {
62 write!(
63 f,
64 "unknown time unit {:?}, \
65 supported units: ns, us, ms, sec, min, hours, days, \
66 weeks, months, years (and few variations)",
67 unit
68 )
69 }
70 Error::NumberOverflow => write!(f, "number is too large"),
71 Error::Empty => write!(f, "value was empty"),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct FormattedDuration(Duration);
79
80trait OverflowOp: Sized {
81 fn mul(self, other: Self) -> Result<Self, Error>;
82 fn add(self, other: Self) -> Result<Self, Error>;
83}
84
85impl OverflowOp for u64 {
86 fn mul(self, other: Self) -> Result<Self, Error> {
87 self.checked_mul(other).ok_or(Error::NumberOverflow)
88 }
89 fn add(self, other: Self) -> Result<Self, Error> {
90 self.checked_add(other).ok_or(Error::NumberOverflow)
91 }
92}
93
94struct Parser<'a> {
95 iter: Chars<'a>,
96 src: &'a str,
97 current: (u64, u64),
98}
99
100impl Parser<'_> {
101 fn off(&self) -> usize {
102 self.src.len() - self.iter.as_str().len()
103 }
104
105 fn parse_first_char(&mut self) -> Result<Option<u64>, Error> {
106 let off = self.off();
107 for c in self.iter.by_ref() {
108 match c {
109 '0'..='9' => {
110 return Ok(Some(c as u64 - '0' as u64));
111 }
112 c if c.is_whitespace() => continue,
113 _ => {
114 return Err(Error::NumberExpected(off));
115 }
116 }
117 }
118 Ok(None)
119 }
120 fn parse_unit(&mut self, n: u64, start: usize, end: usize) -> Result<(), Error> {
121 let (mut sec, nsec) = match &self.src[start..end] {
122 "nanos" | "nsec" | "ns" => (0u64, n),
123 "usec" | "us" => (0u64, n.mul(1000)?),
124 "millis" | "msec" | "ms" => (0u64, n.mul(1_000_000)?),
125 "seconds" | "second" | "secs" | "sec" | "s" => (n, 0),
126 "minutes" | "minute" | "min" | "mins" | "m" => (n.mul(60)?, 0),
127 "hours" | "hour" | "hr" | "hrs" | "h" => (n.mul(3600)?, 0),
128 "days" | "day" | "d" => (n.mul(86400)?, 0),
129 "weeks" | "week" | "w" => (n.mul(86400 * 7)?, 0),
130 "months" | "month" | "M" => (n.mul(2_630_016)?, 0), "years" | "year" | "y" => (n.mul(31_557_600)?, 0), _ => {
133 return Err(Error::UnknownUnit {
134 start,
135 end,
136 unit: self.src[start..end].to_string(),
137 value: n,
138 });
139 }
140 };
141 let mut nsec = self.current.1.add(nsec)?;
142 if nsec > 1_000_000_000 {
143 sec = sec.add(nsec / 1_000_000_000)?;
144 nsec %= 1_000_000_000;
145 }
146 sec = self.current.0.add(sec)?;
147 self.current = (sec, nsec);
148 Ok(())
149 }
150
151 fn parse(mut self) -> Result<Duration, Error> {
152 let mut n = self.parse_first_char()?.ok_or(Error::Empty)?;
153 'outer: loop {
154 let mut off = self.off();
155 while let Some(c) = self.iter.next() {
156 match c {
157 '0'..='9' => {
158 n = n
159 .checked_mul(10)
160 .and_then(|x| x.checked_add(c as u64 - '0' as u64))
161 .ok_or(Error::NumberOverflow)?;
162 }
163 c if c.is_whitespace() => {}
164 'a'..='z' | 'A'..='Z' => {
165 break;
166 }
167 _ => {
168 return Err(Error::InvalidCharacter(off));
169 }
170 }
171 off = self.off();
172 }
173 let start = off;
174 let mut off = self.off();
175 while let Some(c) = self.iter.next() {
176 match c {
177 '0'..='9' => {
178 self.parse_unit(n, start, off)?;
179 n = c as u64 - '0' as u64;
180 continue 'outer;
181 }
182 c if c.is_whitespace() => break,
183 'a'..='z' | 'A'..='Z' => {}
184 _ => {
185 return Err(Error::InvalidCharacter(off));
186 }
187 }
188 off = self.off();
189 }
190 self.parse_unit(n, start, off)?;
191 n = match self.parse_first_char()? {
192 Some(n) => n,
193 None => return Ok(Duration::new(self.current.0, self.current.1 as u32)),
194 };
195 }
196 }
197}
198
199pub fn parse_duration(s: &str) -> Result<Duration, Error> {
225 Parser {
226 iter: s.chars(),
227 src: s,
228 current: (0, 0),
229 }
230 .parse()
231}
232
233pub fn format_duration(val: Duration) -> FormattedDuration {
251 FormattedDuration(val)
252}
253
254fn item_plural(f: &mut fmt::Formatter, started: &mut bool, name: &str, value: u64) -> fmt::Result {
255 if value > 0 {
256 if *started {
257 f.write_str(" ")?;
258 }
259 write!(f, "{}{}", value, name)?;
260 if value > 1 {
261 f.write_str("s")?;
262 }
263 *started = true;
264 }
265 Ok(())
266}
267fn item(f: &mut fmt::Formatter, started: &mut bool, name: &str, value: u32) -> fmt::Result {
268 if value > 0 {
269 if *started {
270 f.write_str(" ")?;
271 }
272 write!(f, "{}{}", value, name)?;
273 *started = true;
274 }
275 Ok(())
276}
277
278impl FormattedDuration {
279 pub fn get_ref(&self) -> &Duration {
281 &self.0
282 }
283}
284
285impl fmt::Display for FormattedDuration {
286 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
287 let secs = self.0.as_secs();
288 let nanos = self.0.subsec_nanos();
289
290 if secs == 0 && nanos == 0 {
291 f.write_str("0s")?;
292 return Ok(());
293 }
294
295 let years = secs / 31_557_600; let ydays = secs % 31_557_600;
297 let months = ydays / 2_630_016; let mdays = ydays % 2_630_016;
299 let days = mdays / 86400;
300 let day_secs = mdays % 86400;
301 let hours = day_secs / 3600;
302 let minutes = day_secs % 3600 / 60;
303 let seconds = day_secs % 60;
304
305 let millis = nanos / 1_000_000;
306 let micros = nanos / 1000 % 1000;
307 let nanosec = nanos % 1000;
308
309 let started = &mut false;
310 item_plural(f, started, "year", years)?;
311 item_plural(f, started, "month", months)?;
312 item_plural(f, started, "day", days)?;
313 item(f, started, "h", hours as u32)?;
314 item(f, started, "m", minutes as u32)?;
315 item(f, started, "s", seconds as u32)?;
316 item(f, started, "ms", millis)?;
317 item(f, started, "us", micros)?;
318 item(f, started, "ns", nanosec)?;
319 Ok(())
320 }
321}
322
323#[cfg(test)]
324mod test {
325 use std::time::Duration;
326
327 use rand::Rng;
328
329 use super::Error;
330 use super::{format_duration, parse_duration};
331
332 #[test]
333 #[allow(clippy::cognitive_complexity)]
334 fn test_units() {
335 assert_eq!(parse_duration("17nsec"), Ok(Duration::new(0, 17)));
336 assert_eq!(parse_duration("17nanos"), Ok(Duration::new(0, 17)));
337 assert_eq!(parse_duration("33ns"), Ok(Duration::new(0, 33)));
338 assert_eq!(parse_duration("3usec"), Ok(Duration::new(0, 3000)));
339 assert_eq!(parse_duration("78us"), Ok(Duration::new(0, 78000)));
340 assert_eq!(parse_duration("31msec"), Ok(Duration::new(0, 31_000_000)));
341 assert_eq!(parse_duration("31millis"), Ok(Duration::new(0, 31_000_000)));
342 assert_eq!(parse_duration("6ms"), Ok(Duration::new(0, 6_000_000)));
343 assert_eq!(parse_duration("3000s"), Ok(Duration::new(3000, 0)));
344 assert_eq!(parse_duration("300sec"), Ok(Duration::new(300, 0)));
345 assert_eq!(parse_duration("300secs"), Ok(Duration::new(300, 0)));
346 assert_eq!(parse_duration("50seconds"), Ok(Duration::new(50, 0)));
347 assert_eq!(parse_duration("1second"), Ok(Duration::new(1, 0)));
348 assert_eq!(parse_duration("100m"), Ok(Duration::new(6000, 0)));
349 assert_eq!(parse_duration("12min"), Ok(Duration::new(720, 0)));
350 assert_eq!(parse_duration("12mins"), Ok(Duration::new(720, 0)));
351 assert_eq!(parse_duration("1minute"), Ok(Duration::new(60, 0)));
352 assert_eq!(parse_duration("7minutes"), Ok(Duration::new(420, 0)));
353 assert_eq!(parse_duration("2h"), Ok(Duration::new(7200, 0)));
354 assert_eq!(parse_duration("7hr"), Ok(Duration::new(25200, 0)));
355 assert_eq!(parse_duration("7hrs"), Ok(Duration::new(25200, 0)));
356 assert_eq!(parse_duration("1hour"), Ok(Duration::new(3600, 0)));
357 assert_eq!(parse_duration("24hours"), Ok(Duration::new(86400, 0)));
358 assert_eq!(parse_duration("1day"), Ok(Duration::new(86400, 0)));
359 assert_eq!(parse_duration("2days"), Ok(Duration::new(172_800, 0)));
360 assert_eq!(parse_duration("365d"), Ok(Duration::new(31_536_000, 0)));
361 assert_eq!(parse_duration("1week"), Ok(Duration::new(604_800, 0)));
362 assert_eq!(parse_duration("7weeks"), Ok(Duration::new(4_233_600, 0)));
363 assert_eq!(parse_duration("52w"), Ok(Duration::new(31_449_600, 0)));
364 assert_eq!(parse_duration("1month"), Ok(Duration::new(2_630_016, 0)));
365 assert_eq!(
366 parse_duration("3months"),
367 Ok(Duration::new(3 * 2_630_016, 0))
368 );
369 assert_eq!(parse_duration("12M"), Ok(Duration::new(31_560_192, 0)));
370 assert_eq!(parse_duration("1year"), Ok(Duration::new(31_557_600, 0)));
371 assert_eq!(
372 parse_duration("7years"),
373 Ok(Duration::new(7 * 31_557_600, 0))
374 );
375 assert_eq!(parse_duration("17y"), Ok(Duration::new(536_479_200, 0)));
376 }
377
378 #[test]
379 fn test_combo() {
380 assert_eq!(
381 parse_duration("20 min 17 nsec "),
382 Ok(Duration::new(1200, 17))
383 );
384 assert_eq!(parse_duration("2h 15m"), Ok(Duration::new(8100, 0)));
385 }
386
387 #[test]
388 fn all_86400_seconds() {
389 for second in 0..86400 {
390 let d = Duration::new(second, 0);
392 assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap());
393 }
394 }
395
396 #[test]
397 fn random_second() {
398 for _ in 0..10000 {
399 let sec = rand::rng().random_range(0..253_370_764_800);
400 let d = Duration::new(sec, 0);
401 assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap());
402 }
403 }
404
405 #[test]
406 fn random_any() {
407 for _ in 0..10000 {
408 let sec = rand::rng().random_range(0..253_370_764_800);
409 let nanos = rand::rng().random_range(0..1_000_000_000);
410 let d = Duration::new(sec, nanos);
411 assert_eq!(d, parse_duration(&format_duration(d).to_string()).unwrap());
412 }
413 }
414
415 #[test]
416 fn test_overlow() {
417 assert_eq!(
420 parse_duration("100000000000000000000ns"),
421 Err(Error::NumberOverflow)
422 );
423 assert_eq!(
424 parse_duration("100000000000000000us"),
425 Err(Error::NumberOverflow)
426 );
427 assert_eq!(
428 parse_duration("100000000000000ms"),
429 Err(Error::NumberOverflow)
430 );
431
432 assert_eq!(
433 parse_duration("100000000000000000000s"),
434 Err(Error::NumberOverflow)
435 );
436 assert_eq!(
437 parse_duration("10000000000000000000m"),
438 Err(Error::NumberOverflow)
439 );
440 assert_eq!(
441 parse_duration("1000000000000000000h"),
442 Err(Error::NumberOverflow)
443 );
444 assert_eq!(
445 parse_duration("100000000000000000d"),
446 Err(Error::NumberOverflow)
447 );
448 assert_eq!(
449 parse_duration("10000000000000000w"),
450 Err(Error::NumberOverflow)
451 );
452 assert_eq!(
453 parse_duration("1000000000000000M"),
454 Err(Error::NumberOverflow)
455 );
456 assert_eq!(
457 parse_duration("10000000000000y"),
458 Err(Error::NumberOverflow)
459 );
460 }
461
462 #[test]
463 fn test_nice_error_message() {
464 assert_eq!(
465 parse_duration("123").unwrap_err().to_string(),
466 "time unit needed, for example 123sec or 123ms"
467 );
468 assert_eq!(
469 parse_duration("10 months 1").unwrap_err().to_string(),
470 "time unit needed, for example 1sec or 1ms"
471 );
472 assert_eq!(
473 parse_duration("10nights").unwrap_err().to_string(),
474 "unknown time unit \"nights\", supported units: \
475 ns, us, ms, sec, min, hours, days, weeks, months, \
476 years (and few variations)"
477 );
478 }
479
480 #[test]
481 fn test_error_cases() {
482 assert_eq!(
483 parse_duration("\0").unwrap_err().to_string(),
484 "expected number at 0"
485 );
486 assert_eq!(
487 parse_duration("\r").unwrap_err().to_string(),
488 "value was empty"
489 );
490 assert_eq!(
491 parse_duration("1~").unwrap_err().to_string(),
492 "invalid character at 1"
493 );
494 assert_eq!(
495 parse_duration("1Nå").unwrap_err().to_string(),
496 "invalid character at 2"
497 );
498 assert_eq!(parse_duration("222nsec221nanosmsec7s5msec572s").unwrap_err().to_string(),
499 "unknown time unit \"nanosmsec\", supported units: ns, us, ms, sec, min, hours, days, weeks, months, years (and few variations)");
500 }
501}