typst_utils/
duration.rs

1use std::fmt::{self, Display, Formatter, Write};
2use std::time::Duration;
3
4use super::round_with_precision;
5
6/// Formats a duration with a precision suitable for human display.
7pub fn format_duration(duration: Duration) -> impl Display {
8    DurationDisplay(duration)
9}
10
11/// Displays a `Duration`.
12struct DurationDisplay(Duration);
13
14impl Display for DurationDisplay {
15    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
16        let mut space = false;
17        macro_rules! piece {
18            ($($tts:tt)*) => {
19                if std::mem::replace(&mut space, true) {
20                    f.write_char(' ')?;
21                }
22                write!(f, $($tts)*)?;
23            };
24        }
25
26        let secs = self.0.as_secs();
27        let (mins, secs) = (secs / 60, (secs % 60));
28        let (hours, mins) = (mins / 60, (mins % 60));
29        let (days, hours) = ((hours / 24), (hours % 24));
30
31        if days > 0 {
32            piece!("{days} d");
33        }
34
35        if hours > 0 {
36            piece!("{hours} h");
37        }
38
39        if mins > 0 {
40            piece!("{mins} min");
41        }
42
43        // No need to display anything more than minutes at this point.
44        if days > 0 || hours > 0 {
45            return Ok(());
46        }
47
48        let order = |exp| 1000u64.pow(exp);
49        let nanos = secs * order(3) + self.0.subsec_nanos() as u64;
50        let fract = |exp| round_with_precision(nanos as f64 / order(exp) as f64, 2);
51
52        if nanos == 0 || self.0 > Duration::from_secs(1) {
53            // For durations > 5 min, we drop the fractional part.
54            if self.0 > Duration::from_secs(300) {
55                piece!("{secs} s");
56            } else {
57                piece!("{} s", fract(3));
58            }
59        } else if self.0 > Duration::from_millis(1) {
60            piece!("{} ms", fract(2));
61        } else if self.0 > Duration::from_micros(1) {
62            piece!("{} µs", fract(1));
63        } else {
64            piece!("{} ns", fract(0));
65        }
66
67        Ok(())
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[track_caller]
76    fn test(duration: Duration, expected: &str) {
77        assert_eq!(format_duration(duration).to_string(), expected);
78    }
79
80    #[test]
81    fn test_format_duration() {
82        test(Duration::from_secs(1000000), "11 d 13 h 46 min");
83        test(Duration::from_secs(3600 * 24), "1 d");
84        test(Duration::from_secs(3600), "1 h");
85        test(Duration::from_secs(3600 + 240), "1 h 4 min");
86        test(Duration::from_secs_f64(364.77), "6 min 4 s");
87        test(Duration::from_secs_f64(264.776), "4 min 24.78 s");
88        test(Duration::from_secs(3), "3 s");
89        test(Duration::from_secs_f64(2.8492), "2.85 s");
90        test(Duration::from_micros(734), "734 µs");
91        test(Duration::from_micros(294816), "294.82 ms");
92        test(Duration::from_nanos(1), "1 ns");
93    }
94}