Skip to main content

harn_cli/
format.rs

1//! Shared, lightweight formatting helpers for CLI commands.
2//!
3//! Multiple subcommands had grown private copies of the same RFC3339
4//! timestamp / human-friendly duration formatters; consolidating them
5//! here keeps formatting consistent across the user-facing surface.
6
7use std::time::Duration as StdDuration;
8
9use time::format_description::well_known::Rfc3339;
10use time::OffsetDateTime;
11
12/// Render a UTC instant as RFC3339, falling back to `Display` if formatting
13/// fails (which `time` only does on internally inconsistent components).
14pub(crate) fn format_timestamp_rfc3339(value: OffsetDateTime) -> String {
15    value.format(&Rfc3339).unwrap_or_else(|_| value.to_string())
16}
17
18/// Render a unix-epoch millisecond timestamp as RFC3339.
19pub(crate) fn format_unix_ms_rfc3339(ms: i64) -> String {
20    let seconds = ms.div_euclid(1000);
21    let value = OffsetDateTime::from_unix_timestamp(seconds).unwrap_or(OffsetDateTime::UNIX_EPOCH);
22    format_timestamp_rfc3339(value)
23}
24
25/// Render a `std::time::Duration` as a coarse "5s / 2m / 3h / 1d / 1w" suffix.
26///
27/// Rounds toward zero on the chosen unit so status output does not overstate
28/// elapsed time.
29pub(crate) fn format_duration_coarse(value: StdDuration) -> String {
30    if value.as_secs() == 0 {
31        return format!("{}ms", value.as_millis());
32    }
33    let seconds = value.as_secs();
34    if seconds < 60 {
35        return format!("{seconds}s");
36    }
37    if seconds < 60 * 60 {
38        return format!("{}m", seconds / 60);
39    }
40    if seconds < 60 * 60 * 24 {
41        return format!("{}h", seconds / (60 * 60));
42    }
43    if seconds < 60 * 60 * 24 * 7 {
44        return format!("{}d", seconds / (60 * 60 * 24));
45    }
46    if seconds.is_multiple_of(60 * 60 * 24 * 7) {
47        return format!("{}w", seconds / (60 * 60 * 24 * 7));
48    }
49    format!("{}d", seconds / (60 * 60 * 24))
50}
51
52/// Render a millisecond duration with a single decimal point of precision
53/// for the ">= 1s" cases (used by portal output).
54pub(crate) fn format_duration_ms(duration_ms: u64) -> String {
55    if duration_ms >= 60_000 {
56        format!("{:.1}m", duration_ms as f64 / 60_000.0)
57    } else if duration_ms >= 1_000 {
58        format!("{:.1}s", duration_ms as f64 / 1_000.0)
59    } else {
60        format!("{duration_ms}ms")
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn timestamp_uses_rfc3339() {
70        let value = OffsetDateTime::UNIX_EPOCH;
71        assert_eq!(format_timestamp_rfc3339(value), "1970-01-01T00:00:00Z");
72    }
73
74    #[test]
75    fn unix_ms_rounds_to_seconds() {
76        assert_eq!(format_unix_ms_rfc3339(0), "1970-01-01T00:00:00Z");
77        assert_eq!(format_unix_ms_rfc3339(1500), "1970-01-01T00:00:01Z");
78        // Negative ms before the epoch should not panic.
79        assert_eq!(format_unix_ms_rfc3339(-1), "1969-12-31T23:59:59Z");
80    }
81
82    #[test]
83    fn coarse_duration_picks_a_unit() {
84        assert_eq!(format_duration_coarse(StdDuration::from_millis(0)), "0ms");
85        assert_eq!(format_duration_coarse(StdDuration::from_secs(5)), "5s");
86        assert_eq!(format_duration_coarse(StdDuration::from_secs(120)), "2m");
87        assert_eq!(format_duration_coarse(StdDuration::from_secs(7200)), "2h");
88        assert_eq!(
89            format_duration_coarse(StdDuration::from_secs(86_400 * 3)),
90            "3d"
91        );
92        assert_eq!(
93            format_duration_coarse(StdDuration::from_secs(86_400 * 14)),
94            "2w"
95        );
96    }
97
98    #[test]
99    fn ms_duration_uses_one_decimal_above_a_second() {
100        assert_eq!(format_duration_ms(500), "500ms");
101        assert_eq!(format_duration_ms(1_500), "1.5s");
102        assert_eq!(format_duration_ms(90_000), "1.5m");
103    }
104}