Skip to main content

haz_cli/output/
duration.rs

1//! Human-readable [`Duration`] formatting for the per-task
2//! summary lines and the run-level epilogue.
3//!
4//! The grammar picks the two coarsest magnitudes that fit:
5//!
6//! | Duration         | Rendered as     |
7//! | ---------------- | --------------- |
8//! | `< 1s`           | `176ms`         |
9//! | `< 1m`           | `7s 856ms`      |
10//! | `< 1h`           | `1m 23s`        |
11//! | `>= 1h`          | `1h 23m`        |
12//!
13//! Sub-millisecond durations round to the nearest millisecond
14//! (clamping at `1ms` so a sub-millisecond run does not display
15//! as `0ms`). The grammar deliberately drops fractional digits;
16//! readers want order-of-magnitude information, not microsecond
17//! precision.
18
19use std::time::Duration;
20
21/// Render `duration` in the grammar described at the module
22/// level. Allocates a single [`String`].
23#[must_use]
24pub fn format_duration(duration: Duration) -> String {
25    let total_secs = duration.as_secs();
26    let sub_secs_millis = duration.subsec_millis();
27
28    if total_secs == 0 {
29        // Sub-second: render in milliseconds, clamping to at
30        // least 1ms so a sub-millisecond duration is not
31        // misrendered as `0ms`.
32        let min_millis = u32::from(!duration.is_zero());
33        let millis = sub_secs_millis.max(min_millis);
34        return format!("{millis}ms");
35    }
36
37    if total_secs < 60 {
38        return format!("{total_secs}s {sub_secs_millis}ms");
39    }
40
41    let total_minutes = total_secs / 60;
42    let seconds_part = total_secs % 60;
43    if total_minutes < 60 {
44        return format!("{total_minutes}m {seconds_part}s");
45    }
46
47    let total_hours = total_minutes / 60;
48    let minutes_part = total_minutes % 60;
49    format!("{total_hours}h {minutes_part}m")
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn sub_millisecond_renders_as_one_ms() {
58        assert_eq!(format_duration(Duration::from_micros(500)), "1ms");
59    }
60
61    #[test]
62    fn zero_renders_as_zero_ms() {
63        assert_eq!(format_duration(Duration::ZERO), "0ms");
64    }
65
66    #[test]
67    fn millisecond_grain_under_one_second() {
68        assert_eq!(format_duration(Duration::from_millis(176)), "176ms");
69        assert_eq!(format_duration(Duration::from_millis(999)), "999ms");
70    }
71
72    #[test]
73    fn second_plus_millisecond_under_one_minute() {
74        assert_eq!(format_duration(Duration::from_millis(7_856)), "7s 856ms");
75        assert_eq!(format_duration(Duration::from_secs(1)), "1s 0ms");
76        assert_eq!(format_duration(Duration::from_millis(59_999)), "59s 999ms");
77    }
78
79    #[test]
80    fn minute_plus_second_under_one_hour() {
81        assert_eq!(format_duration(Duration::from_mins(1)), "1m 0s");
82        assert_eq!(format_duration(Duration::from_secs(83)), "1m 23s");
83        assert_eq!(format_duration(Duration::from_secs(3_599)), "59m 59s");
84    }
85
86    #[test]
87    fn hour_plus_minute() {
88        assert_eq!(format_duration(Duration::from_hours(1)), "1h 0m");
89        assert_eq!(format_duration(Duration::from_mins(83)), "1h 23m");
90    }
91}