#![doc = include_str!("../README.md")]
#![no_std]
#![deny(clippy::pedantic)]
#![deny(clippy::nursery)]
#![forbid(clippy::indexing_slicing)]
#![forbid(clippy::panic)]
#![forbid(clippy::unwrap_used)]
#![forbid(clippy::expect_used)]
#![forbid(clippy::unreachable)]
#![forbid(clippy::todo)]
#![forbid(clippy::unimplemented)]
#![forbid(clippy::alloc_instead_of_core)]
#![forbid(clippy::float_arithmetic)]
#![forbid(clippy::cast_possible_wrap)]
#![forbid(clippy::cast_possible_truncation)]
#![forbid(unsafe_code)]
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteFormatter {
unit: Unit,
standard: Standard,
space: bool,
}
impl Default for ByteFormatter {
fn default() -> Self {
Self::new()
}
}
impl ByteFormatter {
#[must_use]
pub const fn new() -> Self {
Self {
unit: Unit::Bytes,
standard: Standard::Binary,
space: true,
}
}
#[must_use]
pub const fn standard(mut self, standard: Standard) -> Self {
self.standard = standard;
self
}
#[must_use]
pub const fn unit(mut self, unit: Unit) -> Self {
self.unit = unit;
self
}
#[must_use]
pub const fn space(mut self, space: bool) -> Self {
self.space = space;
self
}
#[must_use]
pub fn format(self, val: u64) -> FormattedBytes {
FormattedBytes::from_formatter(val, self.unit, self.standard, self.space)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Unit {
Bytes,
Bits,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Standard {
SI,
Binary,
}
#[derive(Clone, Copy)]
pub struct FormattedBytes {
buf: [u8; 16],
len: usize,
}
impl FormattedBytes {
pub(crate) fn from_formatter(val: u64, unit: Unit, standard: Standard, space: bool) -> Self {
let mag = if val == 0 {
0
} else {
match standard {
Standard::SI => (val.ilog10() / 3) as usize,
Standard::Binary => (val.ilog2() / 10) as usize,
}
};
let mag = mag.min(6);
let (mut whole, mut frac) = if mag == 0 {
(val, 0)
} else {
match standard {
Standard::Binary => {
let shift = mag * 10;
let divisor = 1_u64 << shift;
let whole = val >> shift;
let rem = val & (divisor - 1);
let (scaled_rem, final_shift) = if mag == 6 {
(rem >> 7, shift - 7)
} else {
(rem, shift)
};
let rounder = 1_u64 << (final_shift - 1);
let f = ((scaled_rem * 100) + rounder) >> final_shift;
(whole, f)
}
Standard::SI => {
macro_rules! calc_si {
($div:expr) => {{
let w = val / $div;
let r = val % $div;
let (sr, sd) = if mag == 6 {
(r >> 7, $div >> 7)
} else {
(r, $div)
};
let f = (sr * 100 + (sd / 2)) / sd;
(w, f)
}};
}
match mag {
1 => calc_si!(1_000_u64),
2 => calc_si!(1_000_000_u64),
3 => calc_si!(1_000_000_000_u64),
4 => calc_si!(1_000_000_000_000_u64),
5 => calc_si!(1_000_000_000_000_000_u64),
_ => calc_si!(1_000_000_000_000_000_000_u64),
}
}
}
};
if frac >= 100 {
frac = 0;
whole += 1;
}
let mut buf = [0u8; 16];
let mut iter = buf.iter_mut();
let mut push = |b: u8| {
if let Some(slot) = iter.next() {
*slot = b;
}
};
let (num_buf, num_len) = format_small_num(whole);
for b in num_buf.into_iter().take(num_len) {
push(b);
}
if mag != 0 {
push(b'.');
push(b'0' + u8::try_from(frac / 10).unwrap_or(0));
push(b'0' + u8::try_from(frac % 10).unwrap_or(0));
}
if space {
push(b' ');
}
if mag != 0 {
let prefix = match (mag, standard) {
(1, Standard::SI) => b'k',
(1, Standard::Binary) => b'K',
(2, _) => b'M',
(3, _) => b'G',
(4, _) => b'T',
(5, _) => b'P',
_ => b'E',
};
push(prefix);
if standard == Standard::Binary {
push(b'i');
}
}
push(match unit {
Unit::Bytes => b'B',
Unit::Bits => b'b',
});
let len = 16 - iter.len();
Self { buf, len }
}
#[inline]
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.buf.get(..self.len).unwrap_or(&self.buf)
}
#[inline]
pub fn as_str(&self) -> Result<&str, core::str::Utf8Error> {
let bytes = self.buf.get(..self.len).unwrap_or(&self.buf);
core::str::from_utf8(bytes)
}
}
impl fmt::Display for FormattedBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str().map_err(|_| fmt::Error)?)
}
}
#[inline]
fn format_small_num(n: u64) -> ([u8; 4], usize) {
if n < 10 {
([b'0' + u8::try_from(n).unwrap_or(0), 0, 0, 0], 1)
} else if n < 100 {
(
[
b'0' + u8::try_from(n / 10).unwrap_or(0),
b'0' + u8::try_from(n % 10).unwrap_or(0),
0,
0,
],
2,
)
} else if n < 1000 {
(
[
b'0' + u8::try_from(n / 100).unwrap_or(0),
b'0' + u8::try_from((n / 10) % 10).unwrap_or(0),
b'0' + u8::try_from(n % 10).unwrap_or(0),
0,
],
3,
)
} else {
(
[
b'0' + u8::try_from(n / 1000).unwrap_or(0),
b'0' + u8::try_from((n / 100) % 10).unwrap_or(0),
b'0' + u8::try_from((n / 10) % 10).unwrap_or(0),
b'0' + u8::try_from(n % 10).unwrap_or(0),
],
4,
)
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for FormattedBytes {
fn format(&self, fmt: defmt::Formatter) {
if let Ok(text) = self.as_str() {
defmt::write!(fmt, "{=str}", text);
} else {
defmt::write!(fmt, "<prettier-bytes: invalid utf-8>");
}
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::format;
use super::*;
macro_rules! assert_fmt {
($val:expr, $unit:path, $std:path, $space:expr, $expected:expr) => {
let fmt = ByteFormatter::new()
.unit($unit)
.standard($std)
.space($space)
.format($val);
assert_eq!(fmt.as_str().unwrap(), $expected);
};
}
#[test]
fn test_zero() {
assert_fmt!(0, Unit::Bytes, Standard::SI, true, "0 B");
assert_fmt!(0, Unit::Bits, Standard::SI, true, "0 b");
assert_fmt!(0, Unit::Bytes, Standard::Binary, false, "0B");
assert_fmt!(0, Unit::Bits, Standard::Binary, false, "0b");
}
#[test]
fn test_base_units_under_1000() {
assert_fmt!(1, Unit::Bytes, Standard::SI, true, "1 B");
assert_fmt!(12, Unit::Bytes, Standard::Binary, true, "12 B");
assert_fmt!(345, Unit::Bytes, Standard::SI, false, "345B");
assert_fmt!(999, Unit::Bytes, Standard::SI, true, "999 B");
assert_fmt!(999, Unit::Bytes, Standard::Binary, true, "999 B");
}
#[test]
fn test_si_exact_magnitudes() {
assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
assert_fmt!(1_000_000, Unit::Bytes, Standard::SI, true, "1.00 MB");
assert_fmt!(1_000_000_000, Unit::Bytes, Standard::SI, true, "1.00 GB");
assert_fmt!(
1_000_000_000_000,
Unit::Bytes,
Standard::SI,
true,
"1.00 TB"
);
assert_fmt!(
1_000_000_000_000_000,
Unit::Bytes,
Standard::SI,
true,
"1.00 PB"
);
assert_fmt!(
1_000_000_000_000_000_000,
Unit::Bytes,
Standard::SI,
true,
"1.00 EB"
);
}
#[test]
fn test_binary_exact_magnitudes() {
assert_fmt!(1_024, Unit::Bytes, Standard::Binary, true, "1.00 KiB");
assert_fmt!(1_048_576, Unit::Bytes, Standard::Binary, true, "1.00 MiB");
assert_fmt!(
1_073_741_824,
Unit::Bytes,
Standard::Binary,
true,
"1.00 GiB"
);
assert_fmt!(
1_099_511_627_776,
Unit::Bytes,
Standard::Binary,
true,
"1.00 TiB"
);
assert_fmt!(
1_125_899_906_842_624,
Unit::Bytes,
Standard::Binary,
true,
"1.00 PiB"
);
assert_fmt!(
1_152_921_504_606_846_976,
Unit::Bytes,
Standard::Binary,
true,
"1.00 EiB"
);
}
#[test]
fn test_si_vs_binary_difference() {
assert_fmt!(1_000, Unit::Bytes, Standard::SI, true, "1.00 kB");
assert_fmt!(1_000, Unit::Bytes, Standard::Binary, true, "1000 B");
assert_fmt!(1_023, Unit::Bytes, Standard::SI, true, "1.02 kB");
assert_fmt!(1_023, Unit::Bytes, Standard::Binary, true, "1023 B");
}
#[test]
fn test_rounding_and_decimals() {
assert_fmt!(1_500, Unit::Bytes, Standard::SI, true, "1.50 kB");
assert_fmt!(1_536, Unit::Bytes, Standard::Binary, true, "1.50 KiB");
assert_fmt!(1_004, Unit::Bytes, Standard::SI, true, "1.00 kB");
assert_fmt!(1_005, Unit::Bytes, Standard::SI, true, "1.01 kB");
assert_fmt!(1_230_000, Unit::Bytes, Standard::SI, true, "1.23 MB");
}
#[test]
fn test_carry_over_rounding() {
assert_fmt!(999_999, Unit::Bytes, Standard::SI, true, "1000.00 kB");
assert_fmt!(
1_048_575,
Unit::Bytes,
Standard::Binary,
true,
"1024.00 KiB"
);
}
#[test]
fn test_formatting_variations() {
let val = 2_500_000;
assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
assert_fmt!(val, Unit::Bits, Standard::SI, true, "2.50 Mb");
assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
assert_fmt!(val, Unit::Bytes, Standard::Binary, true, "2.38 MiB");
assert_fmt!(val, Unit::Bytes, Standard::SI, true, "2.50 MB");
assert_fmt!(val, Unit::Bytes, Standard::SI, false, "2.50MB");
}
#[test]
fn test_extreme_values() {
assert_fmt!(u64::MAX, Unit::Bytes, Standard::SI, true, "18.45 EB");
assert_fmt!(u64::MAX, Unit::Bytes, Standard::Binary, true, "16.00 EiB");
}
#[test]
fn test_as_bytes() {
let fmt = ByteFormatter::new()
.unit(Unit::Bytes)
.standard(Standard::SI)
.space(false)
.format(1500);
assert_eq!(fmt.as_bytes(), b"1.50kB");
}
#[test]
fn test_number_boundaries() {
assert_fmt!(9, Unit::Bytes, Standard::SI, true, "9 B"); assert_fmt!(10, Unit::Bytes, Standard::SI, true, "10 B"); assert_fmt!(99, Unit::Bytes, Standard::SI, true, "99 B"); assert_fmt!(100, Unit::Bytes, Standard::SI, true, "100 B"); assert_fmt!(999, Unit::Bytes, Standard::SI, true, "999 B"); }
#[test]
fn test_display_trait() {
let fmt = ByteFormatter::new()
.unit(Unit::Bytes)
.standard(Standard::Binary)
.space(true)
.format(1_048_576);
let output = format!("{fmt}");
assert_eq!(output, "1.00 MiB");
}
}