mi6_cli/display/
format.rs

1//! Formatting utilities for numbers, durations, and bytes.
2
3/// Format a large number with comma separators.
4pub fn format_number(n: i64) -> String {
5    let s = n.to_string();
6    let mut result = String::new();
7    let chars: Vec<char> = s.chars().collect();
8
9    for (i, c) in chars.iter().enumerate() {
10        if i > 0 && (chars.len() - i).is_multiple_of(3) {
11            result.push(',');
12        }
13        result.push(*c);
14    }
15
16    result
17}
18
19/// Format bytes as a human-readable string.
20pub fn format_bytes(bytes: u64) -> String {
21    const KB: u64 = 1024;
22    const MB: u64 = KB * 1024;
23    const GB: u64 = MB * 1024;
24
25    if bytes >= GB {
26        format!("{:.1} GB", bytes as f64 / GB as f64)
27    } else if bytes >= MB {
28        format!("{:.1} MB", bytes as f64 / MB as f64)
29    } else if bytes >= KB {
30        format!("{:.1} KB", bytes as f64 / KB as f64)
31    } else {
32        format!("{} B", bytes)
33    }
34}
35
36/// Format a duration in milliseconds as a human-readable string.
37pub fn format_duration(ms: i64) -> String {
38    let seconds = ms / 1000;
39    let minutes = seconds / 60;
40    let hours = minutes / 60;
41
42    if hours > 0 {
43        format!("{}h {}m {}s", hours, minutes % 60, seconds % 60)
44    } else if minutes > 0 {
45        format!("{}m {}s", minutes, seconds % 60)
46    } else {
47        format!("{}s", seconds)
48    }
49}
50
51/// Calculate percentage with zero-division handling.
52pub fn percentage(part: i64, total: i64) -> f64 {
53    if total == 0 {
54        0.0
55    } else {
56        (part as f64 / total as f64) * 100.0
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63
64    #[test]
65    fn test_format_number() {
66        assert_eq!(format_number(0), "0");
67        assert_eq!(format_number(999), "999");
68        assert_eq!(format_number(1000), "1,000");
69        assert_eq!(format_number(1_000_000), "1,000,000");
70    }
71
72    #[test]
73    fn test_format_bytes_zero() {
74        assert_eq!(format_bytes(0), "0 B");
75    }
76
77    #[test]
78    fn test_format_bytes_bytes() {
79        assert_eq!(format_bytes(500), "500 B");
80        assert_eq!(format_bytes(1023), "1023 B");
81    }
82
83    #[test]
84    fn test_format_bytes_kilobytes() {
85        assert_eq!(format_bytes(1024), "1.0 KB");
86        assert_eq!(format_bytes(1536), "1.5 KB");
87        assert_eq!(format_bytes(1024 * 500), "500.0 KB");
88    }
89
90    #[test]
91    fn test_format_bytes_megabytes() {
92        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
93        assert_eq!(format_bytes(1024 * 1024 * 157), "157.0 MB");
94    }
95
96    #[test]
97    fn test_format_bytes_gigabytes() {
98        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
99        assert_eq!(format_bytes(1024 * 1024 * 1024 * 2), "2.0 GB");
100    }
101
102    #[test]
103    fn test_format_duration() {
104        assert_eq!(format_duration(0), "0s");
105        assert_eq!(format_duration(5000), "5s");
106        assert_eq!(format_duration(65_000), "1m 5s");
107        assert_eq!(format_duration(3_665_000), "1h 1m 5s");
108    }
109
110    #[test]
111    #[expect(clippy::float_cmp)]
112    fn test_percentage() {
113        assert_eq!(percentage(0, 0), 0.0);
114        assert_eq!(percentage(50, 100), 50.0);
115        assert_eq!(percentage(1, 4), 25.0);
116    }
117}