use crate::{FromValue, IntoValue, ShellError, Span, Type, Value};
use num_format::{Locale, WriteFormatted};
use serde::{Deserialize, Serialize};
use std::{
char,
fmt::{self, Write},
iter::Sum,
ops::{Add, Mul, Neg, Sub},
str::FromStr,
};
use thiserror::Error;
pub const SUPPORTED_FILESIZE_UNITS: [&str; 13] = [
"B", "kB", "MB", "GB", "TB", "PB", "EB", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB",
];
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
pub struct Filesize(i64);
impl Filesize {
pub const ZERO: Self = Self(0);
pub const MIN: Self = Self(i64::MIN);
pub const MAX: Self = Self(i64::MAX);
pub const fn new(bytes: i64) -> Self {
Self(bytes)
}
pub const fn from_unit(value: i64, unit: FilesizeUnit) -> Option<Self> {
if let Some(bytes) = value.checked_mul(unit.as_bytes() as i64) {
Some(Self(bytes))
} else {
None
}
}
pub const fn get(&self) -> i64 {
self.0
}
pub const fn is_positive(self) -> bool {
self.0.is_positive()
}
pub const fn is_negative(self) -> bool {
self.0.is_negative()
}
pub const fn signum(self) -> Self {
Self(self.0.signum())
}
pub const fn largest_metric_unit(&self) -> FilesizeUnit {
const KB: u64 = FilesizeUnit::KB.as_bytes();
const MB: u64 = FilesizeUnit::MB.as_bytes();
const GB: u64 = FilesizeUnit::GB.as_bytes();
const TB: u64 = FilesizeUnit::TB.as_bytes();
const PB: u64 = FilesizeUnit::PB.as_bytes();
const EB: u64 = FilesizeUnit::EB.as_bytes();
match self.0.unsigned_abs() {
0..KB => FilesizeUnit::B,
KB..MB => FilesizeUnit::KB,
MB..GB => FilesizeUnit::MB,
GB..TB => FilesizeUnit::GB,
TB..PB => FilesizeUnit::TB,
PB..EB => FilesizeUnit::PB,
EB.. => FilesizeUnit::EB,
}
}
pub const fn largest_binary_unit(&self) -> FilesizeUnit {
const KIB: u64 = FilesizeUnit::KiB.as_bytes();
const MIB: u64 = FilesizeUnit::MiB.as_bytes();
const GIB: u64 = FilesizeUnit::GiB.as_bytes();
const TIB: u64 = FilesizeUnit::TiB.as_bytes();
const PIB: u64 = FilesizeUnit::PiB.as_bytes();
const EIB: u64 = FilesizeUnit::EiB.as_bytes();
match self.0.unsigned_abs() {
0..KIB => FilesizeUnit::B,
KIB..MIB => FilesizeUnit::KiB,
MIB..GIB => FilesizeUnit::MiB,
GIB..TIB => FilesizeUnit::GiB,
TIB..PIB => FilesizeUnit::TiB,
PIB..EIB => FilesizeUnit::PiB,
EIB.. => FilesizeUnit::EiB,
}
}
}
impl From<i64> for Filesize {
fn from(value: i64) -> Self {
Self(value)
}
}
impl From<Filesize> for i64 {
fn from(filesize: Filesize) -> Self {
filesize.0
}
}
macro_rules! impl_from {
($($ty:ty),* $(,)?) => {
$(
impl From<$ty> for Filesize {
#[inline]
fn from(value: $ty) -> Self {
Self(value.into())
}
}
impl TryFrom<Filesize> for $ty {
type Error = <i64 as TryInto<$ty>>::Error;
#[inline]
fn try_from(filesize: Filesize) -> Result<Self, Self::Error> {
filesize.0.try_into()
}
}
)*
};
}
impl_from!(u8, i8, u16, i16, u32, i32);
macro_rules! impl_try_from {
($($ty:ty),* $(,)?) => {
$(
impl TryFrom<$ty> for Filesize {
type Error = <$ty as TryInto<i64>>::Error;
#[inline]
fn try_from(value: $ty) -> Result<Self, Self::Error> {
value.try_into().map(Self)
}
}
impl TryFrom<Filesize> for $ty {
type Error = <i64 as TryInto<$ty>>::Error;
#[inline]
fn try_from(filesize: Filesize) -> Result<Self, Self::Error> {
filesize.0.try_into()
}
}
)*
};
}
impl_try_from!(u64, usize, isize);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
pub struct TryFromFloatError(());
impl fmt::Display for TryFromFloatError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "out of range float type conversion attempted")
}
}
impl TryFrom<f64> for Filesize {
type Error = TryFromFloatError;
#[inline]
fn try_from(value: f64) -> Result<Self, Self::Error> {
if i64::MIN as f64 <= value && value <= i64::MAX as f64 {
Ok(Self(value as i64))
} else {
Err(TryFromFloatError(()))
}
}
}
impl TryFrom<f32> for Filesize {
type Error = TryFromFloatError;
#[inline]
fn try_from(value: f32) -> Result<Self, Self::Error> {
if i64::MIN as f32 <= value && value <= i64::MAX as f32 {
Ok(Self(value as i64))
} else {
Err(TryFromFloatError(()))
}
}
}
impl FromValue for Filesize {
fn from_value(value: Value) -> Result<Self, ShellError> {
value.as_filesize()
}
fn expected_type() -> Type {
Type::Filesize
}
}
impl IntoValue for Filesize {
fn into_value(self, span: Span) -> Value {
Value::filesize(self.0, span)
}
}
impl Add for Filesize {
type Output = Option<Self>;
fn add(self, rhs: Self) -> Self::Output {
self.0.checked_add(rhs.0).map(Self)
}
}
impl Sub for Filesize {
type Output = Option<Self>;
fn sub(self, rhs: Self) -> Self::Output {
self.0.checked_sub(rhs.0).map(Self)
}
}
impl Mul<i64> for Filesize {
type Output = Option<Self>;
fn mul(self, rhs: i64) -> Self::Output {
self.0.checked_mul(rhs).map(Self)
}
}
impl Mul<Filesize> for i64 {
type Output = Option<Filesize>;
fn mul(self, rhs: Filesize) -> Self::Output {
self.checked_mul(rhs.0).map(Filesize::new)
}
}
impl Mul<f64> for Filesize {
type Output = Option<Self>;
fn mul(self, rhs: f64) -> Self::Output {
let bytes = ((self.0 as f64) * rhs).round();
if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 {
Some(Self(bytes as i64))
} else {
None
}
}
}
impl Mul<Filesize> for f64 {
type Output = Option<Filesize>;
fn mul(self, rhs: Filesize) -> Self::Output {
let bytes = (self * (rhs.0 as f64)).round();
if i64::MIN as f64 <= bytes && bytes <= i64::MAX as f64 {
Some(Filesize(bytes as i64))
} else {
None
}
}
}
impl Neg for Filesize {
type Output = Option<Self>;
fn neg(self) -> Self::Output {
self.0.checked_neg().map(Self)
}
}
impl Sum<Filesize> for Option<Filesize> {
fn sum<I: Iterator<Item = Filesize>>(iter: I) -> Self {
let mut sum = Filesize::ZERO;
for filesize in iter {
sum = (sum + filesize)?;
}
Some(sum)
}
}
impl fmt::Display for Filesize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", FilesizeFormatter::new().format(*self))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FilesizeUnit {
B,
KB,
MB,
GB,
TB,
PB,
EB,
KiB,
MiB,
GiB,
TiB,
PiB,
EiB,
}
impl FilesizeUnit {
pub const fn as_bytes(&self) -> u64 {
match self {
Self::B => 1,
Self::KB => 10_u64.pow(3),
Self::MB => 10_u64.pow(6),
Self::GB => 10_u64.pow(9),
Self::TB => 10_u64.pow(12),
Self::PB => 10_u64.pow(15),
Self::EB => 10_u64.pow(18),
Self::KiB => 2_u64.pow(10),
Self::MiB => 2_u64.pow(20),
Self::GiB => 2_u64.pow(30),
Self::TiB => 2_u64.pow(40),
Self::PiB => 2_u64.pow(50),
Self::EiB => 2_u64.pow(60),
}
}
pub const fn as_filesize(&self) -> Filesize {
Filesize::new(self.as_bytes() as i64)
}
pub const fn as_str(&self) -> &'static str {
match self {
Self::B => "B",
Self::KB => "kB",
Self::MB => "MB",
Self::GB => "GB",
Self::TB => "TB",
Self::PB => "PB",
Self::EB => "EB",
Self::KiB => "KiB",
Self::MiB => "MiB",
Self::GiB => "GiB",
Self::TiB => "TiB",
Self::PiB => "PiB",
Self::EiB => "EiB",
}
}
pub const fn is_metric(&self) -> bool {
match self {
Self::B | Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => true,
Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => false,
}
}
pub const fn is_binary(&self) -> bool {
match self {
Self::KB | Self::MB | Self::GB | Self::TB | Self::PB | Self::EB => false,
Self::B | Self::KiB | Self::MiB | Self::GiB | Self::TiB | Self::PiB | Self::EiB => true,
}
}
}
impl From<FilesizeUnit> for Filesize {
fn from(unit: FilesizeUnit) -> Self {
unit.as_filesize()
}
}
impl fmt::Display for FilesizeUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
pub struct ParseFilesizeUnitError(());
impl fmt::Display for ParseFilesizeUnitError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "invalid file size unit")
}
}
impl FromStr for FilesizeUnit {
type Err = ParseFilesizeUnitError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"B" => Self::B,
"kB" => Self::KB,
"MB" => Self::MB,
"GB" => Self::GB,
"TB" => Self::TB,
"PB" => Self::PB,
"EB" => Self::EB,
"KiB" => Self::KiB,
"MiB" => Self::MiB,
"GiB" => Self::GiB,
"TiB" => Self::TiB,
"PiB" => Self::PiB,
"EiB" => Self::EiB,
_ => return Err(ParseFilesizeUnitError(())),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FilesizeUnitFormat {
Metric,
Binary,
Unit(FilesizeUnit),
}
impl FilesizeUnitFormat {
pub const fn as_str(&self) -> &'static str {
match self {
Self::Metric => "metric",
Self::Binary => "binary",
Self::Unit(unit) => unit.as_str(),
}
}
pub const fn is_metric(&self) -> bool {
match self {
Self::Metric => true,
Self::Binary => false,
Self::Unit(unit) => unit.is_metric(),
}
}
pub const fn is_binary(&self) -> bool {
match self {
Self::Metric => false,
Self::Binary => true,
Self::Unit(unit) => unit.is_binary(),
}
}
}
impl From<FilesizeUnit> for FilesizeUnitFormat {
fn from(unit: FilesizeUnit) -> Self {
Self::Unit(unit)
}
}
impl fmt::Display for FilesizeUnitFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Error)]
pub struct ParseFilesizeUnitFormatError(());
impl fmt::Display for ParseFilesizeUnitFormatError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "invalid file size unit format")
}
}
impl FromStr for FilesizeUnitFormat {
type Err = ParseFilesizeUnitFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"metric" => Self::Metric,
"binary" => Self::Binary,
s => Self::Unit(s.parse().map_err(|_| ParseFilesizeUnitFormatError(()))?),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct FilesizeFormatter {
unit: FilesizeUnitFormat,
show_unit: bool,
precision: Option<usize>,
locale: Locale,
}
impl FilesizeFormatter {
pub fn new() -> Self {
FilesizeFormatter {
unit: FilesizeUnitFormat::Metric,
show_unit: true,
precision: None,
locale: Locale::en_US_POSIX,
}
}
pub fn unit(mut self, unit: impl Into<FilesizeUnitFormat>) -> Self {
self.unit = unit.into();
self
}
pub fn show_unit(self, show_unit: bool) -> Self {
Self { show_unit, ..self }
}
pub fn precision(mut self, precision: impl Into<Option<usize>>) -> Self {
self.precision = precision.into();
self
}
pub fn locale(mut self, locale: Locale) -> Self {
self.locale = locale;
self
}
pub fn format(&self, filesize: Filesize) -> FormattedFilesize {
FormattedFilesize {
format: *self,
filesize,
}
}
}
impl Default for FilesizeFormatter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct FormattedFilesize {
format: FilesizeFormatter,
filesize: Filesize,
}
impl fmt::Display for FormattedFilesize {
fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Self { filesize, format } = *self;
let FilesizeFormatter {
unit,
show_unit,
precision,
locale,
} = format;
let unit = match unit {
FilesizeUnitFormat::Metric => filesize.largest_metric_unit(),
FilesizeUnitFormat::Binary => filesize.largest_binary_unit(),
FilesizeUnitFormat::Unit(unit) => unit,
};
let Filesize(filesize) = filesize;
let precision = f.precision().or(precision);
let bytes = unit.as_bytes() as i64;
let whole = filesize / bytes;
let fract = (filesize % bytes).unsigned_abs();
f.write_formatted(&whole, &locale)
.map_err(|_| std::fmt::Error)?;
if unit != FilesizeUnit::B && precision != Some(0) && !(precision.is_none() && fract == 0) {
f.write_str(locale.decimal())?;
let bytes = unit.as_bytes();
let mut fract = fract * 10;
let mut i = 0;
loop {
let q = fract / bytes;
let r = fract % bytes;
debug_assert!(q <= 10);
f.write_char(char::from_digit(q as u32, 10).expect("q <= 10"))?;
i += 1;
if r == 0 || precision.is_some_and(|p| i >= p) {
break;
}
fract = r * 10;
}
if let Some(precision) = precision {
for _ in 0..(precision - i) {
f.write_char('0')?;
}
}
}
if show_unit {
write!(f, " {unit}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(1024, FilesizeUnit::KB, "1.024 kB")]
#[case(1024, FilesizeUnit::B, "1024 B")]
#[case(1024, FilesizeUnit::KiB, "1 KiB")]
#[case(3_000_000, FilesizeUnit::MB, "3 MB")]
#[case(3_000_000, FilesizeUnit::KB, "3000 kB")]
fn display_unit(#[case] bytes: i64, #[case] unit: FilesizeUnit, #[case] exp: &str) {
assert_eq!(
exp,
FilesizeFormatter::new()
.unit(unit)
.format(Filesize::new(bytes))
.to_string()
);
}
#[rstest]
#[case(1000, "1000 B")]
#[case(1024, "1 KiB")]
#[case(1025, "1.0009765625 KiB")]
fn display_auto_binary(#[case] val: i64, #[case] exp: &str) {
assert_eq!(
exp,
FilesizeFormatter::new()
.unit(FilesizeUnitFormat::Binary)
.format(Filesize::new(val))
.to_string()
);
}
#[rstest]
#[case(999, "999 B")]
#[case(1000, "1 kB")]
#[case(1024, "1.024 kB")]
fn display_auto_metric(#[case] val: i64, #[case] exp: &str) {
assert_eq!(
exp,
FilesizeFormatter::new()
.unit(FilesizeUnitFormat::Metric)
.format(Filesize::new(val))
.to_string()
);
}
}