Skip to main content

gander_core/
format.rs

1//! Human-readable formatting.
2//!
3//! Called from the renderer only. Nothing here is on the per-process scan path:
4//! `docs/DESIGN.md` §9.4 — a `format!` inside a scan is a bug.
5
6use std::fmt::Write as _;
7
8use arrayvec::ArrayString;
9
10/// Format a byte count using binary IEC units, matching `htop.1` MEMORY SIZES.
11pub fn bytes_human(bytes: u64) -> ArrayString<16> {
12    const UNITS: [&str; 6] = ["B", "K", "M", "G", "T", "P"];
13    let mut out = ArrayString::new();
14    if bytes < 1024 {
15        let _ = write!(out, "{bytes}B");
16        return out;
17    }
18    let mut value = bytes as f64;
19    let mut unit = 0;
20    while value >= 1024.0 && unit + 1 < UNITS.len() {
21        value /= 1024.0;
22        unit += 1;
23    }
24    // htop prints one decimal below 10 units, none above.
25    let _ = if value < 10.0 {
26        write!(out, "{value:.1}{}", UNITS[unit])
27    } else {
28        write!(out, "{value:.0}{}", UNITS[unit])
29    };
30    out
31}
32
33/// Format a duration with explicit units, for places with room to spell it out.
34///
35/// htop prints uptime as `4-23:13:30`, which requires the reader to remember
36/// that the leading field is days. The meter has space, so it says so:
37///
38/// | input | output |
39/// |---|---|
40/// | 45 s | `45s` |
41/// | 13 min 30 s | `13m 30s` |
42/// | 23 h 13 min | `23h 13m` |
43/// | 4 d 23 h | `4d 23h` |
44/// | 400 d | `400d` |
45///
46/// The largest two fields are kept; seconds are dropped once days appear, because
47/// nobody reading a process viewer needs second-resolution uptime measured in
48/// months.
49pub fn duration_verbose(secs: u64) -> ArrayString<24> {
50    let mut out = ArrayString::new();
51    let d = secs / 86_400;
52    let h = (secs % 86_400) / 3_600;
53    let m = (secs % 3_600) / 60;
54    let s = secs % 60;
55
56    let _ = if d > 0 {
57        if h > 0 {
58            write!(out, "{d}d {h}h")
59        } else {
60            write!(out, "{d}d")
61        }
62    } else if h > 0 {
63        if m > 0 {
64            write!(out, "{h}h {m}m")
65        } else {
66            write!(out, "{h}h")
67        }
68    } else if m > 0 {
69        if s > 0 {
70            write!(out, "{m}m {s}s")
71        } else {
72            write!(out, "{m}m")
73        }
74    } else {
75        write!(out, "{s}s")
76    };
77    out
78}
79
80/// Format a duration as `MM:SS` or `H:MM:SS` / `D-HH:MM:SS` (htop's TIME+ style).
81///
82/// Used for table cells, where a compact, fixed-width value matters more than
83/// immediate legibility. Prefer [`duration_verbose`] when there is room.
84pub fn duration_human(secs: u64) -> ArrayString<16> {
85    let mut out = ArrayString::new();
86    let d = secs / 86_400;
87    let h = (secs % 86_400) / 3_600;
88    let m = (secs % 3_600) / 60;
89    let s = secs % 60;
90    let _ = if d > 0 {
91        write!(out, "{d}-{h:02}:{m:02}:{s:02}")
92    } else if h > 0 {
93        write!(out, "{h}:{m:02}:{s:02}")
94    } else {
95        write!(out, "{m:02}:{s:02}")
96    };
97    out
98}
99
100/// Format CPU ticks as a `MM:SS.cc` figure, as the TIME column shows.
101pub fn cpu_time_human(ticks: u64, clk_tck: u64) -> ArrayString<16> {
102    let mut out = ArrayString::new();
103    if clk_tck == 0 {
104        out.push('-');
105        return out;
106    }
107    let centis = ticks * 100 / clk_tck;
108    let m = centis / 6_000;
109    let s = (centis % 6_000) / 100;
110    let c = centis % 100;
111    let _ = write!(out, "{m:02}:{s:02}.{c:02}");
112    out
113}
114
115/// Format a percentage with one decimal, as a fixed-width cell.
116pub fn percent_human(p: f32) -> ArrayString<8> {
117    let mut out = ArrayString::new();
118    if p.is_nan() {
119        out.push('-');
120        return out;
121    }
122    let _ = write!(out, "{p:.1}");
123    out
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn bytes_scaling() {
132        assert_eq!(&*bytes_human(0), "0B");
133        assert_eq!(&*bytes_human(512), "512B");
134        assert_eq!(&*bytes_human(1024), "1.0K");
135        assert_eq!(&*bytes_human(1536), "1.5K");
136        assert_eq!(&*bytes_human(1024 * 1024 * 200), "200M");
137    }
138
139    #[test]
140    fn durations() {
141        assert_eq!(&*duration_human(0), "00:00");
142        assert_eq!(&*duration_human(75), "01:15");
143        assert_eq!(&*duration_human(3_725), "1:02:05");
144        assert_eq!(&*duration_human(90_061), "1-01:01:01");
145    }
146
147    #[test]
148    fn verbose_durations_have_units() {
149        assert_eq!(&*duration_verbose(0), "0s");
150        assert_eq!(&*duration_verbose(45), "45s");
151        assert_eq!(&*duration_verbose(60), "1m");
152        assert_eq!(&*duration_verbose(90), "1m 30s");
153        assert_eq!(&*duration_verbose(3_600), "1h");
154        assert_eq!(&*duration_verbose(3_725), "1h 2m");
155        assert_eq!(&*duration_verbose(86_400), "1d");
156        assert_eq!(&*duration_verbose(90_061), "1d 1h");
157        assert_eq!(
158            &*duration_verbose(4 * 86_400 + 23 * 3_600 + 13 * 60),
159            "4d 23h"
160        );
161    }
162
163    #[test]
164    fn verbose_durations_fit_the_buffer() {
165        // 24 chars: the longest realistic value is a machine up for ~10 years.
166        let ten_years = 10 * 365 * 86_400u64;
167        let text = duration_verbose(ten_years);
168        assert!(!text.is_empty());
169        assert!(text.len() < 24, "got {text:?}");
170    }
171
172    #[test]
173    fn cpu_time() {
174        // 100 ticks at 100 Hz == 1 second
175        assert_eq!(&*cpu_time_human(100, 100), "00:01.00");
176        assert_eq!(&*cpu_time_human(0, 0), "-");
177    }
178}