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" suffix.
26///
27/// Rounds toward zero on the chosen unit, mirroring the original behavior
28/// of the orchestrator command output.
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    format!("{}d", seconds / (60 * 60 * 24))
44}
45
46/// Render a millisecond duration with a single decimal point of precision
47/// for the ">= 1s" cases (used by portal output).
48pub(crate) fn format_duration_ms(duration_ms: u64) -> String {
49    if duration_ms >= 60_000 {
50        format!("{:.1}m", duration_ms as f64 / 60_000.0)
51    } else if duration_ms >= 1_000 {
52        format!("{:.1}s", duration_ms as f64 / 1_000.0)
53    } else {
54        format!("{duration_ms}ms")
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn timestamp_uses_rfc3339() {
64        let value = OffsetDateTime::UNIX_EPOCH;
65        assert_eq!(format_timestamp_rfc3339(value), "1970-01-01T00:00:00Z");
66    }
67
68    #[test]
69    fn unix_ms_rounds_to_seconds() {
70        assert_eq!(format_unix_ms_rfc3339(0), "1970-01-01T00:00:00Z");
71        assert_eq!(format_unix_ms_rfc3339(1500), "1970-01-01T00:00:01Z");
72        // Negative ms before the epoch should not panic.
73        assert_eq!(format_unix_ms_rfc3339(-1), "1969-12-31T23:59:59Z");
74    }
75
76    #[test]
77    fn coarse_duration_picks_a_unit() {
78        assert_eq!(format_duration_coarse(StdDuration::from_millis(0)), "0ms");
79        assert_eq!(format_duration_coarse(StdDuration::from_secs(5)), "5s");
80        assert_eq!(format_duration_coarse(StdDuration::from_secs(120)), "2m");
81        assert_eq!(format_duration_coarse(StdDuration::from_secs(7200)), "2h");
82        assert_eq!(
83            format_duration_coarse(StdDuration::from_secs(86_400 * 3)),
84            "3d"
85        );
86    }
87
88    #[test]
89    fn ms_duration_uses_one_decimal_above_a_second() {
90        assert_eq!(format_duration_ms(500), "500ms");
91        assert_eq!(format_duration_ms(1_500), "1.5s");
92        assert_eq!(format_duration_ms(90_000), "1.5m");
93    }
94}