humantime/
duration.rs

1use std::error::Error as StdError;
2use std::fmt;
3use std::str::Chars;
4use std::time::Duration;
5
6/// Error parsing human-friendly duration
7#[derive(Debug, PartialEq, Clone)]
8pub enum Error {
9    /// Invalid character during parsing
10    ///
11    /// More specifically anything that is not alphanumeric is prohibited
12    ///
13    /// The field is an byte offset of the character in the string.
14    InvalidCharacter(usize),
15    /// Non-numeric value where number is expected
16    ///
17    /// This usually means that either time unit is broken into words,
18    /// e.g. `m sec` instead of `msec`, or just number is omitted,
19    /// for example `2 hours min` instead of `2 hours 1 min`
20    ///
21    /// The field is an byte offset of the errorneous character
22    /// in the string.
23    NumberExpected(usize),
24    /// Unit in the number is not one of allowed units
25    ///
26    /// See documentation of `parse_duration` for the list of supported
27    /// time units.
28    ///
29    /// The two fields are start and end (exclusive) of the slice from
30    /// the original string, containing errorneous value
31    UnknownUnit {
32        /// Start of the invalid unit inside the original string
33        start: usize,
34        /// End of the invalid unit inside the original string
35        end: usize,
36        /// The unit verbatim
37        unit: String,
38        /// A number associated with the unit
39        value: u64,
40    },
41    /// The numeric value is too large
42    ///
43    /// Usually this means value is too large to be useful. If user writes
44    /// data in subsecond units, then the maximum is about 3k years. When
45    /// using seconds, or larger units, the limit is even larger.
46    NumberOverflow,
47    /// The value was an empty string (or consists only whitespace)
48    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/// A wrapper type that allows you to Display a Duration
77#[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), // 30.44d
131            "years" | "year" | "y" => (n.mul(31_557_600)?, 0),  // 365.25d
132            _ => {
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
199/// Parse duration object `1hour 12min 5s`
200///
201/// The duration object is a concatenation of time spans. Where each time
202/// span is an integer number and a suffix. Supported suffixes:
203///
204/// * `nsec`, `ns` -- nanoseconds
205/// * `usec`, `us` -- microseconds
206/// * `msec`, `ms` -- milliseconds
207/// * `seconds`, `second`, `sec`, `s`
208/// * `minutes`, `minute`, `min`, `m`
209/// * `hours`, `hour`, `hr`, `h`
210/// * `days`, `day`, `d`
211/// * `weeks`, `week`, `w`
212/// * `months`, `month`, `M` -- defined as 30.44 days
213/// * `years`, `year`, `y` -- defined as 365.25 days
214///
215/// # Examples
216///
217/// ```
218/// use std::time::Duration;
219/// use humantime::parse_duration;
220///
221/// assert_eq!(parse_duration("2h 37min"), Ok(Duration::new(9420, 0)));
222/// assert_eq!(parse_duration("32ms"), Ok(Duration::new(0, 32_000_000)));
223/// ```
224pub 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
233/// Formats duration into a human-readable string
234///
235/// Note: this format is guaranteed to have same value when using
236/// parse_duration, but we can change some details of the exact composition
237/// of the value.
238///
239/// # Examples
240///
241/// ```
242/// use std::time::Duration;
243/// use humantime::format_duration;
244///
245/// let val1 = Duration::new(9420, 0);
246/// assert_eq!(format_duration(val1).to_string(), "2h 37m");
247/// let val2 = Duration::new(0, 32_000_000);
248/// assert_eq!(format_duration(val2).to_string(), "32ms");
249/// ```
250pub 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    /// Returns a reference to the [`Duration`][] that is being formatted.
280    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; // 365.25d
296        let ydays = secs % 31_557_600;
297        let months = ydays / 2_630_016; // 30.44d
298        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            // scan leap year and non-leap year
391            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        // Overflow on subseconds is earlier because of how we do conversion
418        // we could fix it, but I don't see any good reason for this
419        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}