Skip to main content

rich_rust/
filesize.rs

1//! File size formatting for human-readable output.
2//!
3//! This module provides functions to format byte sizes into human-readable strings,
4//! supporting both binary (1024-based: KiB, MiB, GiB) and decimal (1000-based: KB, MB, GB) units.
5//!
6//! # Examples
7//!
8//! ```
9//! use rich_rust::filesize::{decimal, format_size, SizeUnit};
10//!
11//! // Decimal (1000-based) formatting
12//! assert_eq!(decimal(1_500_000), "1.5 MB");
13//! assert_eq!(decimal(1_000), "1.0 kB");
14//!
15//! // Binary (1024-based) formatting
16//! use rich_rust::filesize::binary;
17//! assert_eq!(binary(1_048_576), "1.0 MiB");
18//! assert_eq!(binary(1_024), "1.0 KiB");
19//!
20//! // Custom precision
21//! use rich_rust::filesize::decimal_with_precision;
22//! assert_eq!(decimal_with_precision(1_536_000, 2), "1.54 MB");
23//! ```
24
25/// Units for binary (1024-based) file sizes.
26const BINARY_UNITS: &[&str] = &[
27    "bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB",
28];
29
30/// Units for decimal (1000-based) file sizes.
31const DECIMAL_UNITS: &[&str] = &["bytes", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
32
33/// Size unit system to use for formatting.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum SizeUnit {
36    /// Binary units (1024-based): KiB, MiB, GiB, etc.
37    #[default]
38    Binary,
39    /// Decimal units (1000-based): kB, MB, GB, etc.
40    Decimal,
41}
42
43/// Format a size in bytes to a human-readable string.
44///
45/// # Arguments
46///
47/// * `size` - Size in bytes (can be negative for deltas)
48/// * `unit` - Whether to use binary (1024) or decimal (1000) units
49/// * `precision` - Number of decimal places
50///
51/// # Examples
52///
53/// ```
54/// use rich_rust::filesize::{format_size, SizeUnit};
55///
56/// assert_eq!(format_size(1_500_000, SizeUnit::Decimal, 1), "1.5 MB");
57/// assert_eq!(format_size(1_048_576, SizeUnit::Binary, 1), "1.0 MiB");
58/// assert_eq!(format_size(-1_000, SizeUnit::Decimal, 1), "-1.0 kB");
59/// ```
60#[must_use]
61pub fn format_size(size: i64, unit: SizeUnit, precision: usize) -> String {
62    let (base, units): (f64, &[&str]) = match unit {
63        SizeUnit::Binary => (1024.0, BINARY_UNITS),
64        SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
65    };
66
67    let negative = size < 0;
68    let abs_size = size.unsigned_abs();
69
70    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
71    if abs_size < base as u64 {
72        // Special case: show as bytes without decimal
73        let prefix = if negative { "-" } else { "" };
74        return format!("{prefix}{abs_size} bytes");
75    }
76
77    #[allow(clippy::cast_precision_loss)]
78    let mut value = abs_size as f64;
79    let mut unit_idx = 0;
80
81    while value >= base && unit_idx < units.len() - 1 {
82        value /= base;
83        unit_idx += 1;
84    }
85
86    let prefix = if negative { "-" } else { "" };
87    format!("{prefix}{value:.precision$} {}", units[unit_idx])
88}
89
90/// Format a size in bytes to a human-readable string using decimal (1000-based) units.
91///
92/// Uses 1 decimal place by default.
93///
94/// # Examples
95///
96/// ```
97/// use rich_rust::filesize::decimal;
98///
99/// assert_eq!(decimal(1_000), "1.0 kB");
100/// assert_eq!(decimal(1_500_000), "1.5 MB");
101/// assert_eq!(decimal(1_000_000_000), "1.0 GB");
102/// ```
103#[must_use]
104pub fn decimal(size: u64) -> String {
105    #[allow(clippy::cast_possible_wrap)]
106    format_size(size as i64, SizeUnit::Decimal, 1)
107}
108
109/// Format a size in bytes to a human-readable string using decimal (1000-based) units
110/// with custom precision.
111///
112/// # Examples
113///
114/// ```
115/// use rich_rust::filesize::decimal_with_precision;
116///
117/// assert_eq!(decimal_with_precision(1_536_000, 2), "1.54 MB");
118/// assert_eq!(decimal_with_precision(1_234_567_890, 3), "1.235 GB");
119/// ```
120#[must_use]
121pub fn decimal_with_precision(size: u64, precision: usize) -> String {
122    #[allow(clippy::cast_possible_wrap)]
123    format_size(size as i64, SizeUnit::Decimal, precision)
124}
125
126/// Format a size in bytes to a human-readable string using binary (1024-based) units.
127///
128/// Uses 1 decimal place by default.
129///
130/// # Examples
131///
132/// ```
133/// use rich_rust::filesize::binary;
134///
135/// assert_eq!(binary(1_024), "1.0 KiB");
136/// assert_eq!(binary(1_048_576), "1.0 MiB");
137/// assert_eq!(binary(1_073_741_824), "1.0 GiB");
138/// ```
139#[must_use]
140pub fn binary(size: u64) -> String {
141    #[allow(clippy::cast_possible_wrap)]
142    format_size(size as i64, SizeUnit::Binary, 1)
143}
144
145/// Format a size in bytes to a human-readable string using binary (1024-based) units
146/// with custom precision.
147///
148/// # Examples
149///
150/// ```
151/// use rich_rust::filesize::binary_with_precision;
152///
153/// assert_eq!(binary_with_precision(1_572_864, 2), "1.50 MiB");
154/// assert_eq!(binary_with_precision(1_073_741_824, 3), "1.000 GiB");
155/// ```
156#[must_use]
157pub fn binary_with_precision(size: u64, precision: usize) -> String {
158    #[allow(clippy::cast_possible_wrap)]
159    format_size(size as i64, SizeUnit::Binary, precision)
160}
161
162/// Format a transfer speed in bytes per second to a human-readable string.
163///
164/// # Arguments
165///
166/// * `bytes_per_second` - Transfer speed in bytes per second
167/// * `unit` - Whether to use binary (1024) or decimal (1000) units
168/// * `precision` - Number of decimal places
169///
170/// # Examples
171///
172/// ```
173/// use rich_rust::filesize::{format_speed, SizeUnit};
174///
175/// assert_eq!(format_speed(1_500_000.0, SizeUnit::Decimal, 1), "1.5 MB/s");
176/// assert_eq!(format_speed(1_048_576.0, SizeUnit::Binary, 1), "1.0 MiB/s");
177/// ```
178#[must_use]
179pub fn format_speed(bytes_per_second: f64, unit: SizeUnit, precision: usize) -> String {
180    // Handle NaN and Infinity gracefully
181    if bytes_per_second.is_nan() {
182        return "NaN".to_string();
183    }
184    if bytes_per_second.is_infinite() {
185        let prefix = if bytes_per_second.is_sign_negative() {
186            "-"
187        } else {
188            ""
189        };
190        return format!("{prefix}∞");
191    }
192
193    let (base, units): (f64, &[&str]) = match unit {
194        SizeUnit::Binary => (1024.0, BINARY_UNITS),
195        SizeUnit::Decimal => (1000.0, DECIMAL_UNITS),
196    };
197
198    let negative = bytes_per_second < 0.0;
199    let mut value = bytes_per_second.abs();
200
201    if value < base {
202        // Show as bytes/s
203        let prefix = if negative { "-" } else { "" };
204        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
205        let int_value = value as u64;
206        return format!("{prefix}{int_value} bytes/s");
207    }
208
209    let mut unit_idx = 0;
210    while value >= base && unit_idx < units.len() - 1 {
211        value /= base;
212        unit_idx += 1;
213    }
214
215    let unit_str = units[unit_idx];
216    // Replace "bytes" with just the unit letter for speed display
217    let speed_unit = if unit_str == "bytes" {
218        "bytes/s"
219    } else {
220        // Append /s to unit
221        &format!("{unit_str}/s")
222    };
223
224    let prefix = if negative { "-" } else { "" };
225    format!("{prefix}{value:.precision$} {speed_unit}")
226}
227
228/// Format a transfer speed using decimal (1000-based) units.
229///
230/// # Examples
231///
232/// ```
233/// use rich_rust::filesize::decimal_speed;
234///
235/// assert_eq!(decimal_speed(1_500_000.0), "1.5 MB/s");
236/// assert_eq!(decimal_speed(500.0), "500 bytes/s");
237/// ```
238#[must_use]
239pub fn decimal_speed(bytes_per_second: f64) -> String {
240    format_speed(bytes_per_second, SizeUnit::Decimal, 1)
241}
242
243/// Format a transfer speed using binary (1024-based) units.
244///
245/// # Examples
246///
247/// ```
248/// use rich_rust::filesize::binary_speed;
249///
250/// assert_eq!(binary_speed(1_048_576.0), "1.0 MiB/s");
251/// assert_eq!(binary_speed(512.0), "512 bytes/s");
252/// ```
253#[must_use]
254pub fn binary_speed(bytes_per_second: f64) -> String {
255    format_speed(bytes_per_second, SizeUnit::Binary, 1)
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_decimal_bytes() {
264        assert_eq!(decimal(0), "0 bytes");
265        assert_eq!(decimal(1), "1 bytes");
266        assert_eq!(decimal(999), "999 bytes");
267    }
268
269    #[test]
270    fn test_decimal_kilobytes() {
271        assert_eq!(decimal(1_000), "1.0 kB");
272        assert_eq!(decimal(1_500), "1.5 kB");
273        assert_eq!(decimal(999_000), "999.0 kB");
274    }
275
276    #[test]
277    fn test_decimal_megabytes() {
278        assert_eq!(decimal(1_000_000), "1.0 MB");
279        assert_eq!(decimal(1_500_000), "1.5 MB");
280        assert_eq!(decimal(999_000_000), "999.0 MB");
281    }
282
283    #[test]
284    fn test_decimal_gigabytes() {
285        assert_eq!(decimal(1_000_000_000), "1.0 GB");
286        assert_eq!(decimal(1_500_000_000), "1.5 GB");
287    }
288
289    #[test]
290    fn test_decimal_terabytes() {
291        assert_eq!(decimal(1_000_000_000_000), "1.0 TB");
292        assert_eq!(decimal(1_500_000_000_000), "1.5 TB");
293    }
294
295    #[test]
296    fn test_binary_bytes() {
297        assert_eq!(binary(0), "0 bytes");
298        assert_eq!(binary(1), "1 bytes");
299        assert_eq!(binary(1023), "1023 bytes");
300    }
301
302    #[test]
303    fn test_binary_kibibytes() {
304        assert_eq!(binary(1_024), "1.0 KiB");
305        assert_eq!(binary(1_536), "1.5 KiB");
306    }
307
308    #[test]
309    fn test_binary_mebibytes() {
310        assert_eq!(binary(1_048_576), "1.0 MiB");
311        assert_eq!(binary(1_572_864), "1.5 MiB");
312    }
313
314    #[test]
315    fn test_binary_gibibytes() {
316        assert_eq!(binary(1_073_741_824), "1.0 GiB");
317        assert_eq!(binary(1_610_612_736), "1.5 GiB");
318    }
319
320    #[test]
321    fn test_precision() {
322        assert_eq!(decimal_with_precision(1_234_567, 0), "1 MB");
323        assert_eq!(decimal_with_precision(1_234_567, 1), "1.2 MB");
324        assert_eq!(decimal_with_precision(1_234_567, 2), "1.23 MB");
325        assert_eq!(decimal_with_precision(1_234_567, 3), "1.235 MB");
326    }
327
328    #[test]
329    fn test_negative_size() {
330        assert_eq!(format_size(-1_000, SizeUnit::Decimal, 1), "-1.0 kB");
331        assert_eq!(format_size(-1_500_000, SizeUnit::Decimal, 1), "-1.5 MB");
332    }
333
334    #[test]
335    fn test_decimal_speed() {
336        assert_eq!(decimal_speed(500.0), "500 bytes/s");
337        assert_eq!(decimal_speed(1_500_000.0), "1.5 MB/s");
338        assert_eq!(decimal_speed(1_000_000_000.0), "1.0 GB/s");
339    }
340
341    #[test]
342    fn test_binary_speed() {
343        assert_eq!(binary_speed(512.0), "512 bytes/s");
344        assert_eq!(binary_speed(1_048_576.0), "1.0 MiB/s");
345        assert_eq!(binary_speed(1_073_741_824.0), "1.0 GiB/s");
346    }
347
348    #[test]
349    fn test_speed_precision() {
350        assert_eq!(format_speed(1_234_567.0, SizeUnit::Decimal, 2), "1.23 MB/s");
351        assert_eq!(format_speed(1_234_567.0, SizeUnit::Binary, 2), "1.18 MiB/s");
352    }
353
354    #[test]
355    fn test_large_sizes() {
356        // Petabytes
357        assert_eq!(decimal(1_000_000_000_000_000), "1.0 PB");
358        assert_eq!(binary(1_125_899_906_842_624), "1.0 PiB");
359
360        // Exabytes
361        assert_eq!(decimal(1_000_000_000_000_000_000), "1.0 EB");
362        assert_eq!(binary(1_152_921_504_606_846_976), "1.0 EiB");
363    }
364
365    #[test]
366    fn test_speed_nan_handling() {
367        assert_eq!(format_speed(f64::NAN, SizeUnit::Decimal, 1), "NaN");
368    }
369
370    #[test]
371    fn test_speed_infinity_handling() {
372        assert_eq!(format_speed(f64::INFINITY, SizeUnit::Decimal, 1), "∞");
373        assert_eq!(format_speed(f64::NEG_INFINITY, SizeUnit::Decimal, 1), "-∞");
374    }
375}