Skip to main content

aria2_core/util/
format.rs

1// Unified formatting utilities for human-readable display of bytes, speeds, and durations.
2//
3// All functions use IEC binary prefixes (KiB, MiB, GiB, TiB) for consistency
4// across the codebase.
5
6const KIB: u64 = 1024;
7const MIB: u64 = 1024 * KIB;
8const GIB: u64 = 1024 * MIB;
9const TIB: u64 = 1024 * GIB;
10
11const SECS_PER_MINUTE: u64 = 60;
12const SECS_PER_HOUR: u64 = 3600;
13
14/// Format a byte count into human-readable form using IEC binary prefixes.
15///
16/// # Arguments
17///
18/// * `bytes` - Raw byte count
19///
20/// # Returns
21///
22/// Formatted string like `"12.3 MiB"` or `"456 KiB"`
23///
24/// # Example
25///
26/// ```
27/// use aria2_core::util::format::format_bytes;
28///
29/// assert_eq!(format_bytes(500), "500 B");
30/// assert_eq!(format_bytes(2048), "2.00 KiB");
31/// assert_eq!(format_bytes(1048576), "1.00 MiB");
32/// ```
33pub fn format_bytes(bytes: u64) -> String {
34    if bytes >= TIB {
35        format!("{:.2} TiB", bytes as f64 / TIB as f64)
36    } else if bytes >= GIB {
37        format!("{:.2} GiB", bytes as f64 / GIB as f64)
38    } else if bytes >= MIB {
39        format!("{:.2} MiB", bytes as f64 / MIB as f64)
40    } else if bytes >= KIB {
41        format!("{:.2} KiB", bytes as f64 / KIB as f64)
42    } else {
43        format!("{} B", bytes)
44    }
45}
46
47/// Format a transfer speed into human-readable form using IEC binary prefixes.
48///
49/// # Arguments
50///
51/// * `bytes_per_sec` - Speed in bytes per second
52///
53/// # Returns
54///
55/// Formatted string like `"2.34 MiB/s"` or `"512 KiB/s"`
56///
57/// # Example
58///
59/// ```
60/// use aria2_core::util::format::format_speed;
61///
62/// assert_eq!(format_speed(1536.0), "1.50 KiB/s");
63/// assert_eq!(format_speed(1048576.0), "1.00 MiB/s");
64/// ```
65pub fn format_speed(bytes_per_sec: f64) -> String {
66    if bytes_per_sec >= TIB as f64 {
67        format!("{:.2} TiB/s", bytes_per_sec / TIB as f64)
68    } else if bytes_per_sec >= GIB as f64 {
69        format!("{:.2} GiB/s", bytes_per_sec / GIB as f64)
70    } else if bytes_per_sec >= MIB as f64 {
71        format!("{:.2} MiB/s", bytes_per_sec / MIB as f64)
72    } else if bytes_per_sec >= KIB as f64 {
73        format!("{:.2} KiB/s", bytes_per_sec / KIB as f64)
74    } else {
75        format!("{:.0} B/s", bytes_per_sec)
76    }
77}
78
79/// Format a duration as a human-readable string with "Xh Ym Zs" style.
80///
81/// # Arguments
82///
83/// * `secs` - Duration in seconds
84///
85/// # Returns
86///
87/// Formatted string like `"1h 23m 45s"`, `"5m 30s"`, or `"45s"`
88///
89/// # Example
90///
91/// ```
92/// use aria2_core::util::format::format_duration;
93///
94/// assert_eq!(format_duration(45), "45s");
95/// assert_eq!(format_duration(125), "2m 5s");
96/// assert_eq!(format_duration(3661), "1h 1m 1s");
97/// ```
98pub fn format_duration(secs: u64) -> String {
99    let hours = secs / SECS_PER_HOUR;
100    let minutes = (secs % SECS_PER_HOUR) / SECS_PER_MINUTE;
101    let seconds = secs % SECS_PER_MINUTE;
102
103    if hours > 0 {
104        format!("{}h {}m {}s", hours, minutes, seconds)
105    } else if minutes > 0 {
106        format!("{}m {}s", minutes, seconds)
107    } else {
108        format!("{}s", seconds)
109    }
110}
111
112/// Format a duration as a compact string suitable for UI display.
113///
114/// Produces compact representations:
115/// - Zero: `"0s"`
116/// - Seconds only: `"42s"`
117/// - Minutes + seconds: `"3m12s"`
118/// - Hours + minutes + seconds: `"1h23m45s"`
119///
120/// # Arguments
121///
122/// * `secs` - Duration in seconds
123///
124/// # Returns
125///
126/// Short formatted string like `"3m12s"` or `"1h23m45s"`
127///
128/// # Example
129///
130/// ```
131/// use aria2_core::util::format::format_duration_short;
132///
133/// assert_eq!(format_duration_short(0), "0s");
134/// assert_eq!(format_duration_short(45), "45s");
135/// assert_eq!(format_duration_short(125), "2m5s");
136/// assert_eq!(format_duration_short(3661), "1h1m1s");
137/// ```
138pub fn format_duration_short(secs: u64) -> String {
139    if secs == 0 {
140        return "0s".to_string();
141    }
142    let h = secs / SECS_PER_HOUR;
143    let m = (secs % SECS_PER_HOUR) / SECS_PER_MINUTE;
144    let s = secs % SECS_PER_MINUTE;
145    if h > 0 {
146        format!("{}h{}m{}s", h, m, s)
147    } else if m > 0 {
148        format!("{}m{}s", m, s)
149    } else {
150        format!("{}s", s)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_format_bytes() {
160        assert_eq!(format_bytes(500), "500 B");
161        assert_eq!(format_bytes(2048), "2.00 KiB");
162        assert_eq!(format_bytes(1048576), "1.00 MiB");
163        assert_eq!(format_bytes(1073741824), "1.00 GiB");
164        assert_eq!(format_bytes(1099511627776), "1.00 TiB");
165    }
166
167    #[test]
168    fn test_format_speed() {
169        assert_eq!(format_speed(500.0), "500 B/s");
170        assert_eq!(format_speed(1536.0), "1.50 KiB/s");
171        assert_eq!(format_speed(1048576.0), "1.00 MiB/s");
172        assert_eq!(format_speed(1073741824.0), "1.00 GiB/s");
173        assert_eq!(format_speed(1099511627776.0), "1.00 TiB/s");
174    }
175
176    #[test]
177    fn test_format_duration() {
178        assert_eq!(format_duration(0), "0s");
179        assert_eq!(format_duration(45), "45s");
180        assert_eq!(format_duration(125), "2m 5s");
181        assert_eq!(format_duration(3661), "1h 1m 1s");
182    }
183
184    #[test]
185    fn test_format_duration_short() {
186        assert_eq!(format_duration_short(0), "0s");
187        assert_eq!(format_duration_short(1), "1s");
188        assert_eq!(format_duration_short(59), "59s");
189        assert_eq!(format_duration_short(60), "1m0s");
190        assert_eq!(format_duration_short(61), "1m1s");
191        assert_eq!(format_duration_short(3599), "59m59s");
192        assert_eq!(format_duration_short(3600), "1h0m0s");
193        assert_eq!(format_duration_short(3661), "1h1m1s");
194    }
195}