Skip to main content

microsandbox_utils/
format.rs

1//! Small display formatters shared across the workspace.
2//!
3//! Lives in `microsandbox-utils` so the CLI (`crates/cli`), the metrics
4//! collector (`crates/metrics-collector`), and anything else that needs
5//! human-readable byte counts or durations can use a single
6//! implementation. Output style is fixed (binary units for bytes,
7//! `<mins>m<secs>s` for durations) so output across surfaces stays
8//! consistent.
9
10use std::time::Duration;
11
12/// Format a byte count with binary units (`B`, `KiB`, `MiB`, `GiB`,
13/// `TiB`). Values below `1 KiB` are rendered as the exact byte count;
14/// everything larger uses one decimal.
15///
16/// ```
17/// use microsandbox_utils::format::format_bytes;
18/// assert_eq!(format_bytes(0), "0 B");
19/// assert_eq!(format_bytes(1023), "1023 B");
20/// assert_eq!(format_bytes(1024), "1.0 KiB");
21/// assert_eq!(format_bytes(14_628_864), "14.0 MiB");
22/// ```
23pub fn format_bytes(bytes: u64) -> String {
24    const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
25
26    let mut value = bytes as f64;
27    let mut unit = 0usize;
28    while value >= 1024.0 && unit < UNITS.len() - 1 {
29        value /= 1024.0;
30        unit += 1;
31    }
32
33    if unit == 0 {
34        format!("{bytes} {}", UNITS[unit])
35    } else {
36        format!("{value:.1} {}", UNITS[unit])
37    }
38}
39
40/// Format a duration for display. Sub-minute values are rendered as
41/// `<seconds.1>s`; longer ones as `<mins>m<remaining-secs>s`.
42///
43/// ```
44/// use std::time::Duration;
45/// use microsandbox_utils::format::format_duration;
46/// assert_eq!(format_duration(Duration::from_secs_f64(12.3)), "12.3s");
47/// assert_eq!(format_duration(Duration::from_secs(125)), "2m5s");
48/// ```
49pub fn format_duration(d: Duration) -> String {
50    let secs = d.as_secs_f64();
51    if secs < 60.0 {
52        format!("{secs:.1}s")
53    } else {
54        let mins = secs as u64 / 60;
55        let remaining = secs as u64 % 60;
56        format!("{mins}m{remaining}s")
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn bytes_renders_each_unit_boundary() {
66        assert_eq!(format_bytes(0), "0 B");
67        assert_eq!(format_bytes(1023), "1023 B");
68        assert_eq!(format_bytes(1024), "1.0 KiB");
69        assert_eq!(format_bytes(1024 * 1024), "1.0 MiB");
70        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GiB");
71        assert_eq!(format_bytes(1024 * 1024 * 1024 * 1024), "1.0 TiB");
72    }
73
74    #[test]
75    fn bytes_uses_one_decimal_above_one_kib() {
76        assert_eq!(format_bytes(14 * 1024 * 1024), "14.0 MiB");
77        assert_eq!(format_bytes(14_628_864), "14.0 MiB");
78    }
79
80    #[test]
81    fn duration_under_minute_uses_seconds() {
82        assert_eq!(format_duration(Duration::from_secs_f64(0.0)), "0.0s");
83        assert_eq!(format_duration(Duration::from_secs_f64(59.9)), "59.9s");
84    }
85
86    #[test]
87    fn duration_over_minute_uses_mins_secs() {
88        assert_eq!(format_duration(Duration::from_secs(60)), "1m0s");
89        assert_eq!(format_duration(Duration::from_secs(125)), "2m5s");
90        assert_eq!(format_duration(Duration::from_secs(3661)), "61m1s");
91    }
92}