net_bytes/
file_size.rs

1//! File size formatting utilities
2//!
3//! Provides functionality for formatting file sizes with support for both
4//! SI (base-1000) and IEC (base-1024) standards.
5//!
6//! 提供文件大小格式化功能,支持 SI (base-1000) 和 IEC (base-1024) 两种标准。
7
8use rust_decimal::Decimal;
9
10use crate::FileSizeFormat;
11use std::num::NonZeroU64;
12use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
13
14/// A non-zero file size representation
15///
16/// 一个表示非零文件大小的结构体。
17#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
18pub struct NonZeroFileSize {
19    bytes: NonZeroU64,
20}
21
22impl NonZeroFileSize {
23    /// Create a new FileSize instance
24    ///
25    /// 创建一个新的 FileSize 实例
26    #[inline]
27    pub fn new(bytes: NonZeroU64) -> Self {
28        Self { bytes }
29    }
30
31    /// Get the inner NonZeroU64 byte value
32    ///
33    /// 获取内部的 NonZeroU64 字节值
34    #[inline]
35    pub fn get_nonzero(&self) -> NonZeroU64 {
36        self.bytes
37    }
38
39    /// Get the byte value as u64
40    ///
41    /// 以 u64 的形式获取字节值
42    #[inline]
43    pub fn as_u64(&self) -> u64 {
44        self.bytes.get()
45    }
46}
47
48// Implement FileSizeFormat for FileSize
49impl FileSizeFormat for NonZeroFileSize {
50    /// Returns the formatted value and unit in SI (base-1000) standard
51    ///
52    /// The formatted_value is returned as String to ensure correct decimal places (e.g., "1.00")
53    ///
54    /// 返回 SI (base-1000) 标准的 (formatted_value, unit)
55    ///
56    /// formatted_value 作为 String 返回,以保证正确的小数位数 (例如 "1.00")
57    #[inline]
58    fn get_si_parts(&self) -> (String, &'static str) {
59        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
60        crate::format_parts(
61            rust_decimal::Decimal::from(self.bytes.get()),
62            rust_decimal::Decimal::from(1000),
63            UNITS,
64        )
65    }
66
67    /// Returns the formatted value and unit in IEC (base-1024) standard
68    ///
69    /// The formatted_value is returned as String to ensure correct decimal places (e.g., "1.00")
70    ///
71    /// 返回 IEC (base-1024) 标准的 (formatted_value, unit)
72    ///
73    /// formatted_value 作为 String 返回,以保证正确的小数位数 (例如 "1.00")
74    #[inline]
75    fn get_iec_parts(&self) -> (String, &'static str) {
76        format_iec_parts(self.bytes.get())
77    }
78}
79
80/// A file size representation that can be zero
81///
82/// 一个可以表示零的文件大小结构体。
83#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
84pub struct FileSize {
85    bytes: u64,
86}
87
88impl FileSize {
89    /// Create a new FileSize instance from bytes
90    ///
91    /// 从字节数创建一个新的 FileSize 实例
92    #[inline]
93    pub const fn new(bytes: u64) -> Self {
94        Self { bytes }
95    }
96
97    /// Get the byte value as u64
98    ///
99    /// 以 u64 的形式获取字节值
100    #[inline]
101    pub const fn as_u64(&self) -> u64 {
102        self.bytes
103    }
104
105    /// Convert to NonZeroFileSize if the value is not zero
106    ///
107    /// 如果值不为零,则转换为 NonZeroFileSize
108    #[inline]
109    pub fn to_nonzero(&self) -> Option<NonZeroFileSize> {
110        NonZeroU64::new(self.bytes).map(NonZeroFileSize::new)
111    }
112}
113
114// Implement arithmetic operations for FileSize
115impl Add for FileSize {
116    type Output = Self;
117
118    #[inline]
119    fn add(self, rhs: Self) -> Self::Output {
120        Self {
121            bytes: self.bytes + rhs.bytes,
122        }
123    }
124}
125
126impl Sub for FileSize {
127    type Output = Self;
128
129    #[inline]
130    fn sub(self, rhs: Self) -> Self::Output {
131        Self {
132            bytes: self.bytes.saturating_sub(rhs.bytes),
133        }
134    }
135}
136
137impl Mul<u64> for FileSize {
138    type Output = Self;
139
140    #[inline]
141    fn mul(self, rhs: u64) -> Self::Output {
142        Self {
143            bytes: self.bytes.saturating_mul(rhs),
144        }
145    }
146}
147
148impl Div<u64> for FileSize {
149    type Output = Self;
150
151    #[inline]
152    fn div(self, rhs: u64) -> Self::Output {
153        Self {
154            bytes: if rhs == 0 { 0 } else { self.bytes / rhs },
155        }
156    }
157}
158
159// Implement assignment operators
160impl AddAssign for FileSize {
161    #[inline]
162    fn add_assign(&mut self, rhs: Self) {
163        self.bytes = self.bytes.saturating_add(rhs.bytes);
164    }
165}
166
167impl SubAssign for FileSize {
168    #[inline]
169    fn sub_assign(&mut self, rhs: Self) {
170        self.bytes = self.bytes.saturating_sub(rhs.bytes);
171    }
172}
173
174impl MulAssign<u64> for FileSize {
175    #[inline]
176    fn mul_assign(&mut self, rhs: u64) {
177        self.bytes = self.bytes.saturating_mul(rhs);
178    }
179}
180
181impl DivAssign<u64> for FileSize {
182    #[inline]
183    fn div_assign(&mut self, rhs: u64) {
184        if rhs != 0 {
185            self.bytes /= rhs;
186        } else {
187            self.bytes = 0;
188        }
189    }
190}
191
192// Implement From and TryFrom for conversions
193impl From<u64> for FileSize {
194    #[inline]
195    fn from(bytes: u64) -> Self {
196        Self::new(bytes)
197    }
198}
199
200impl From<NonZeroFileSize> for FileSize {
201    #[inline]
202    fn from(size: NonZeroFileSize) -> Self {
203        Self::new(size.as_u64())
204    }
205}
206
207impl TryFrom<FileSize> for NonZeroFileSize {
208    type Error = std::num::TryFromIntError;
209
210    #[inline]
211    fn try_from(value: FileSize) -> Result<Self, Self::Error> {
212        NonZeroU64::try_from(value.bytes).map(NonZeroFileSize::new)
213    }
214}
215
216// Implement FileSizeFormat for FileSize
217impl FileSizeFormat for FileSize {
218    /// Returns the formatted value and unit in SI (base-1000) standard
219    ///
220    /// The formatted_value is returned as String to ensure correct decimal places (e.g., "1.00")
221    ///
222    /// 返回 SI (base-1000) 标准的 (formatted_value, unit)
223    ///
224    /// formatted_value 作为 String 返回,以保证正确的小数位数 (例如 "1.00")
225    #[inline]
226    fn get_si_parts(&self) -> (String, &'static str) {
227        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
228        crate::format_parts(Decimal::from(self.bytes), Decimal::from(1000), UNITS)
229    }
230
231    /// Returns the formatted value and unit in IEC (base-1024) standard
232    ///
233    /// The formatted_value is returned as String to ensure correct decimal places (e.g., "1.00")
234    ///
235    /// 返回 IEC (base-1024) 标准的 (formatted_value, unit)
236    ///
237    /// formatted_value 作为 String 返回,以保证正确的小数位数 (例如 "1.00")
238    #[inline]
239    fn get_iec_parts(&self) -> (String, &'static str) {
240        format_iec_parts(self.bytes)
241    }
242}
243
244/// Format bytes using IEC (base-1024) units
245///
246/// 使用 IEC (base-1024) 单位格式化字节数
247fn format_iec_parts(bytes: u64) -> (String, &'static str) {
248    const UNITS: [&str; 9] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
249    const BASE: u64 = 1024;
250
251    let mut value = bytes as f64;
252    let mut unit_index = 0;
253
254    while value >= (BASE as f64) && unit_index < UNITS.len() - 1 {
255        value /= BASE as f64;
256        unit_index += 1;
257    }
258
259    // Format with appropriate decimal places
260    let formatted = if value < 10.0 {
261        // Always show 2 decimal places for values < 10
262        format!("{:.2}", value)
263    } else {
264        // Always show 1 decimal place for values >= 10
265        format!("{:.1}", value)
266    };
267
268    (formatted, UNITS[unit_index])
269}
270
271// 单元测试
272#[cfg(test)]
273mod tests {
274    use crate::{FileSizeFormat, FormattedValue, SizeStandard};
275
276    use super::{FileSize, NonZeroFileSize};
277    use std::num::NonZeroU64;
278
279    /// Helper function - SI standard
280    ///
281    /// 辅助函数 - SI 标准
282    fn format_test_si(bytes: u64) -> String {
283        let nz = NonZeroU64::new(bytes).expect("测试值不能为零");
284        FormattedValue::new(NonZeroFileSize::new(nz), SizeStandard::SI).to_string()
285    }
286
287    /// Helper function - IEC standard
288    ///
289    /// 辅助函数 - IEC 标准
290    fn format_test_iec(bytes: u64) -> String {
291        let nz = NonZeroU64::new(bytes).expect("测试值不能为零");
292        FormattedValue::new(NonZeroFileSize::new(nz), SizeStandard::IEC).to_string()
293    }
294
295    // --- Tests for SI (base-1000) standard ---
296    // --- SI (base-1000) 测试 ---
297    #[test]
298    fn test_si_bytes() {
299        assert_eq!(format_test_si(512), "512.0 B");
300        assert_eq!(format_test_si(999), "999.0 B");
301    }
302
303    #[test]
304    fn test_si_kilobytes() {
305        assert_eq!(format_test_si(1000), "1.00 KB");
306        // 1.024 -> 1.02 (2 位小数)
307        assert_eq!(format_test_si(1024), "1.02 KB");
308        // 9.999 -> 10.00 (2 位小数, 舍入)
309        assert_eq!(format_test_si(9999), "10.00 KB");
310        // 10.0 (1 位小数)
311        assert_eq!(format_test_si(10000), "10.0 KB");
312        // 100.0 (1 位小数)
313        assert_eq!(format_test_si(100000), "100.0 KB");
314    }
315
316    #[test]
317    fn test_si_megabytes() {
318        assert_eq!(format_test_si(1_000_000), "1.00 MB");
319    }
320
321    // --- Tests for IEC (base-1024) standard ---
322    // --- IEC (base-1024) 测试 ---
323    #[test]
324    fn test_iec_bytes() {
325        assert_eq!(format_test_iec(512), "512.0 B");
326        assert_eq!(format_test_iec(1023), "1023.0 B");
327    }
328
329    #[test]
330    fn test_iec_kibibytes() {
331        assert_eq!(format_test_iec(1024), "1.00 KiB");
332        // 1.464... -> 1.46 (2 位小数)
333        assert_eq!(format_test_iec(1500), "1.46 KiB");
334        // 9.999... -> 10.00 (2 位小数, 舍入)
335        let bytes_near_10 = (9.999 * 1024.0) as u64;
336        assert_eq!(format_test_iec(bytes_near_10), "10.00 KiB");
337        // 10.0 (1 位小数)
338        assert_eq!(format_test_iec(10 * 1024), "10.0 KiB");
339        // 100.0 (1 位小数)
340        assert_eq!(format_test_iec(100 * 1024), "100.0 KiB");
341    }
342
343    #[test]
344    fn test_iec_mebibytes() {
345        assert_eq!(format_test_iec(1024 * 1024), "1.00 MiB");
346    }
347
348    // --- Tests for FileSize ---
349    // --- FileSize 测试 ---
350    #[test]
351    fn test_filesize_arithmetic() {
352        let size1 = FileSize::new(1024);
353        let size2 = FileSize::new(2048);
354
355        // Addition
356        assert_eq!((size1 + size2).as_u64(), 3072);
357
358        // Subtraction
359        assert_eq!((size2 - size1).as_u64(), 1024);
360        assert_eq!((size1 - size2).as_u64(), 0); // Saturating sub
361
362        // Multiplication
363        assert_eq!((size1 * 2).as_u64(), 2048);
364
365        // Division
366        assert_eq!((size2 / 2).as_u64(), 1024);
367
368        // Division by zero
369        assert_eq!((size1 / 0).as_u64(), 0);
370    }
371
372    #[test]
373    fn test_filesize_formatting() {
374        let size = FileSize::new(1500);
375
376        // Test SI formatting
377        let (value, unit) = size.get_si_parts();
378        assert_eq!(&value, "1.50");
379        assert_eq!(unit, "KB");
380
381        // Test IEC formatting
382        let (value, unit) = size.get_iec_parts();
383        assert_eq!(&value, "1.46");
384        assert_eq!(unit, "KiB");
385    }
386
387    #[test]
388    fn test_filesize_conversions() {
389        // From u64
390        let size = FileSize::from(1024u64);
391        assert_eq!(size.as_u64(), 1024);
392
393        // From NonZeroFileSize
394        let non_zero = NonZeroFileSize::new(NonZeroU64::new(2048).unwrap());
395        let size = FileSize::from(non_zero);
396        assert_eq!(size.as_u64(), 2048);
397
398        // To NonZeroFileSize
399        let size = FileSize::new(1024);
400        let non_zero = NonZeroFileSize::try_from(size);
401        assert!(non_zero.is_ok());
402        assert_eq!(non_zero.unwrap().as_u64(), 1024);
403
404        // Zero to NonZeroFileSize should fail
405        let zero = FileSize::new(0);
406        let result = NonZeroFileSize::try_from(zero);
407        assert!(result.is_err());
408    }
409}