use crate::{FileSizeFormat, format_parts_scaled, SCALE};
use std::num::NonZeroU64;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct NonZeroFileSize {
bytes: NonZeroU64,
}
impl NonZeroFileSize {
#[inline]
pub fn new(bytes: NonZeroU64) -> Self {
Self { bytes }
}
#[inline]
pub fn get_nonzero(&self) -> NonZeroU64 {
self.bytes
}
#[inline]
pub fn as_u64(&self) -> u64 {
self.bytes.get()
}
}
impl FileSizeFormat for NonZeroFileSize {
#[inline]
fn get_si_parts(&self) -> (String, &'static str) {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
format_parts_scaled((self.bytes.get() as u128) * SCALE, 1000, UNITS)
}
#[inline]
fn get_iec_parts(&self) -> (String, &'static str) {
format_iec_parts(self.bytes.get())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FileSize {
bytes: u64,
}
impl FileSize {
#[inline]
pub const fn new(bytes: u64) -> Self {
Self { bytes }
}
#[inline]
pub const fn as_u64(&self) -> u64 {
self.bytes
}
#[inline]
pub fn to_nonzero(&self) -> Option<NonZeroFileSize> {
NonZeroU64::new(self.bytes).map(NonZeroFileSize::new)
}
}
impl Add for FileSize {
type Output = Self;
#[inline]
fn add(self, rhs: Self) -> Self::Output {
Self {
bytes: self.bytes + rhs.bytes,
}
}
}
impl Sub for FileSize {
type Output = Self;
#[inline]
fn sub(self, rhs: Self) -> Self::Output {
Self {
bytes: self.bytes.saturating_sub(rhs.bytes),
}
}
}
impl Mul<u64> for FileSize {
type Output = Self;
#[inline]
fn mul(self, rhs: u64) -> Self::Output {
Self {
bytes: self.bytes.saturating_mul(rhs),
}
}
}
impl Div<u64> for FileSize {
type Output = Self;
#[inline]
fn div(self, rhs: u64) -> Self::Output {
Self {
bytes: if rhs == 0 { 0 } else { self.bytes / rhs },
}
}
}
impl AddAssign for FileSize {
#[inline]
fn add_assign(&mut self, rhs: Self) {
self.bytes = self.bytes.saturating_add(rhs.bytes);
}
}
impl SubAssign for FileSize {
#[inline]
fn sub_assign(&mut self, rhs: Self) {
self.bytes = self.bytes.saturating_sub(rhs.bytes);
}
}
impl MulAssign<u64> for FileSize {
#[inline]
fn mul_assign(&mut self, rhs: u64) {
self.bytes = self.bytes.saturating_mul(rhs);
}
}
impl DivAssign<u64> for FileSize {
#[inline]
fn div_assign(&mut self, rhs: u64) {
if rhs != 0 {
self.bytes /= rhs;
} else {
self.bytes = 0;
}
}
}
impl From<u64> for FileSize {
#[inline]
fn from(bytes: u64) -> Self {
Self::new(bytes)
}
}
impl From<NonZeroFileSize> for FileSize {
#[inline]
fn from(size: NonZeroFileSize) -> Self {
Self::new(size.as_u64())
}
}
impl TryFrom<FileSize> for NonZeroFileSize {
type Error = std::num::TryFromIntError;
#[inline]
fn try_from(value: FileSize) -> Result<Self, Self::Error> {
NonZeroU64::try_from(value.bytes).map(NonZeroFileSize::new)
}
}
impl FileSizeFormat for FileSize {
#[inline]
fn get_si_parts(&self) -> (String, &'static str) {
const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
format_parts_scaled((self.bytes as u128) * SCALE, 1000, UNITS)
}
#[inline]
fn get_iec_parts(&self) -> (String, &'static str) {
format_iec_parts(self.bytes)
}
}
fn format_iec_parts(bytes: u64) -> (String, &'static str) {
const UNITS: [&str; 9] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
const BASE: u64 = 1024;
let mut value = bytes as f64;
let mut unit_index = 0;
while value >= (BASE as f64) && unit_index < UNITS.len() - 1 {
value /= BASE as f64;
unit_index += 1;
}
let formatted = if value < 10.0 {
format!("{:.2}", value)
} else {
format!("{:.1}", value)
};
(formatted, UNITS[unit_index])
}
#[cfg(test)]
mod tests {
use crate::{FileSizeFormat, FormattedValue, SizeStandard};
use super::{FileSize, NonZeroFileSize};
use std::num::NonZeroU64;
fn format_test_si(bytes: u64) -> String {
let nz = NonZeroU64::new(bytes).expect("测试值不能为零");
FormattedValue::new(NonZeroFileSize::new(nz), SizeStandard::SI).to_string()
}
fn format_test_iec(bytes: u64) -> String {
let nz = NonZeroU64::new(bytes).expect("测试值不能为零");
FormattedValue::new(NonZeroFileSize::new(nz), SizeStandard::IEC).to_string()
}
#[test]
fn test_si_bytes() {
assert_eq!(format_test_si(512), "512.0 B");
assert_eq!(format_test_si(999), "999.0 B");
}
#[test]
fn test_si_kilobytes() {
assert_eq!(format_test_si(1000), "1.00 KB");
assert_eq!(format_test_si(1024), "1.02 KB");
assert_eq!(format_test_si(9999), "10.00 KB");
assert_eq!(format_test_si(10000), "10.0 KB");
assert_eq!(format_test_si(100000), "100.0 KB");
}
#[test]
fn test_si_megabytes() {
assert_eq!(format_test_si(1_000_000), "1.00 MB");
}
#[test]
fn test_iec_bytes() {
assert_eq!(format_test_iec(512), "512.0 B");
assert_eq!(format_test_iec(1023), "1023.0 B");
}
#[test]
fn test_iec_kibibytes() {
assert_eq!(format_test_iec(1024), "1.00 KiB");
assert_eq!(format_test_iec(1500), "1.46 KiB");
let bytes_near_10 = (9.999 * 1024.0) as u64;
assert_eq!(format_test_iec(bytes_near_10), "10.00 KiB");
assert_eq!(format_test_iec(10 * 1024), "10.0 KiB");
assert_eq!(format_test_iec(100 * 1024), "100.0 KiB");
}
#[test]
fn test_iec_mebibytes() {
assert_eq!(format_test_iec(1024 * 1024), "1.00 MiB");
}
#[test]
fn test_filesize_arithmetic() {
let size1 = FileSize::new(1024);
let size2 = FileSize::new(2048);
assert_eq!((size1 + size2).as_u64(), 3072);
assert_eq!((size2 - size1).as_u64(), 1024);
assert_eq!((size1 - size2).as_u64(), 0);
assert_eq!((size1 * 2).as_u64(), 2048);
assert_eq!((size2 / 2).as_u64(), 1024);
assert_eq!((size1 / 0).as_u64(), 0);
}
#[test]
fn test_filesize_formatting() {
let size = FileSize::new(1500);
let (value, unit) = size.get_si_parts();
assert_eq!(&value, "1.50");
assert_eq!(unit, "KB");
let (value, unit) = size.get_iec_parts();
assert_eq!(&value, "1.46");
assert_eq!(unit, "KiB");
}
#[test]
fn test_filesize_conversions() {
let size = FileSize::from(1024u64);
assert_eq!(size.as_u64(), 1024);
let non_zero = NonZeroFileSize::new(NonZeroU64::new(2048).unwrap());
let size = FileSize::from(non_zero);
assert_eq!(size.as_u64(), 2048);
let size = FileSize::new(1024);
let non_zero = NonZeroFileSize::try_from(size);
assert!(non_zero.is_ok());
assert_eq!(non_zero.unwrap().as_u64(), 1024);
let zero = FileSize::new(0);
let result = NonZeroFileSize::try_from(zero);
assert!(result.is_err());
}
}