#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SizeBase {
Binary,
Decimal,
}
const BINARY_UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
const DECIMAL_UNITS: [&str; 7] = ["B", "kB", "MB", "GB", "TB", "PB", "EB"];
impl SizeBase {
const fn step(self) -> u64 {
match self {
Self::Binary => 1024,
Self::Decimal => 1000,
}
}
const fn units(self) -> &'static [&'static str; 7] {
match self {
Self::Binary => &BINARY_UNITS,
Self::Decimal => &DECIMAL_UNITS,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum SizeShape {
Single { decimals: usize },
Significant { digits: usize },
#[allow(dead_code)]
Composite { parts: usize },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct SizeFormat {
pub base: SizeBase,
pub shape: SizeShape,
}
impl SizeFormat {
pub(crate) const fn binary() -> Self {
Self {
base: SizeBase::Binary,
shape: SizeShape::Single { decimals: 2 },
}
}
pub(crate) const fn decimal() -> Self {
Self {
base: SizeBase::Decimal,
shape: SizeShape::Single { decimals: 2 },
}
}
pub(crate) const fn with_decimals(self, decimals: usize) -> Self {
Self {
shape: SizeShape::Single { decimals },
..self
}
}
#[allow(dead_code)]
pub(crate) const fn with_parts(self, parts: usize) -> Self {
Self {
shape: SizeShape::Composite { parts },
..self
}
}
pub(crate) const fn with_significant(self, digits: usize) -> Self {
Self {
shape: SizeShape::Significant { digits },
..self
}
}
#[allow(dead_code)]
pub(crate) const fn default_parts(self) -> Self {
self.with_parts(DEFAULT_PARTS)
}
}
#[derive(Clone, Copy)]
enum Precision {
Fixed(usize),
Significant(usize),
}
impl Precision {
fn decimals_for(self, value: f64) -> usize {
match self {
Self::Fixed(decimals) => decimals,
Self::Significant(digits) => {
let whole = value.trunc().abs();
let before_the_point = if whole < 1.0 {
1
} else {
whole.log10().floor() as usize + 1
};
digits.saturating_sub(before_the_point)
}
}
}
}
pub(crate) const DEFAULT_PARTS: usize = 3;
pub(crate) fn format_bytes(bytes: u64, fmt: &SizeFormat) -> String {
match fmt.shape {
SizeShape::Single { decimals } => single(bytes, fmt.base, Precision::Fixed(decimals)),
SizeShape::Significant { digits } => {
single(bytes, fmt.base, Precision::Significant(digits))
}
SizeShape::Composite { parts } => composite(bytes, fmt.base, parts),
}
}
fn single(bytes: u64, base: SizeBase, precision: Precision) -> String {
let units = base.units();
let step = base.step();
let mut index = 0;
let mut divisor = 1u64;
while index + 1 < units.len() && bytes / divisor >= step {
divisor *= step;
index += 1;
}
if index == 0 {
return format!("{bytes}B");
}
let mut value = bytes as f64 / divisor as f64;
let mut decimals = precision.decimals_for(value);
let rounds_onto_the_next_unit = format!("{value:.decimals$}")
.parse::<f64>()
.is_ok_and(|rounded| rounded >= step as f64);
if rounds_onto_the_next_unit && index + 1 < units.len() {
divisor *= step;
index += 1;
value = bytes as f64 / divisor as f64;
decimals = precision.decimals_for(value);
}
format!("{value:.decimals$}{}", units[index])
}
fn composite(bytes: u64, base: SizeBase, parts: usize) -> String {
let units = base.units();
let step = base.step();
let wanted = parts.max(1);
let mut divisor = 1u64;
for _ in 1..units.len() {
divisor *= step;
}
let mut remainder = bytes;
let mut out: Vec<String> = Vec::with_capacity(wanted);
for index in (0..units.len()).rev() {
if out.len() == wanted {
break;
}
let count = remainder / divisor;
if count > 0 {
out.push(format!("{count}{}", units[index]));
remainder %= divisor;
}
if index > 0 {
divisor /= step;
}
}
if out.is_empty() {
return "0B".to_string();
}
out.join(" ")
}
#[cfg(test)]
#[path = "bytes_tests.rs"]
mod tests;