// This file is port from https://github.com/bytesize-rs/bytesize/blob/7467a23df30bc1fee48326e0fa6c063fc85676a2/src/display.rs
// Copyright Apache-2.0 January 2004 The bytesize Authors. All rights reserved.
///|
/// Formatting style used when rendering a `ByteSize`.
///
/// `Format` controls the unit family (IEC or SI), whether separators are kept,
/// and how suffixes such as `B` or `iB` are produced.
pub enum Format {
Iec
IecShort
Si
SiShort
} derive(Debug)
///|
pub impl Show for Format with fn to_string(self) {
repr(self)
}
///|
/// Returns the numeric unit step for this format.
///
/// IEC formats scale by `1024`, while SI formats scale by `1000`. `Display`
/// uses this value to decide when to promote bytes to the next unit.
pub fn Format::unit(self : Format) -> UInt64 {
match self {
Iec | IecShort => KIB
Si | SiShort => KB
}
}
///|
/// Returns the logarithmic base used for unit selection.
///
/// This value matches the natural logarithm of the format's unit step and is
/// used by `Display::to_string` to choose the best prefix efficiently.
pub fn Format::unit_base(self : Format) -> Double {
match self {
Iec | IecShort => LN_KIB
Si | SiShort => LN_KB
}
}
///|
/// Returns the sequence of unit prefixes for this format.
///
/// IEC formats use uppercase binary prefixes such as `K`, `M`, and `G`, while
/// SI formats use the decimal sequence beginning with lowercase `k`.
pub fn Format::unit_prefixes(self : Format) -> String {
match self {
Iec | IecShort => UNITS_IEC
Si | SiShort => UNITS_SI
}
}
///|
/// Returns the separator inserted between the numeric value and unit text.
///
/// Long formats keep a space, such as `1 KiB` or `1 MB`, while the short
/// formats omit it to produce strings such as `1K` and `1M`.
pub fn Format::unit_separator(self : Format) -> String {
match self {
Iec | Si => " "
IecShort | SiShort => ""
}
}
///|
/// Returns the suffix appended after the unit prefix.
///
/// IEC long format uses `iB`, SI long format uses `B`, and the short formats
/// omit the suffix entirely.
pub fn Format::unit_suffix(self : Format) -> String {
match self {
Iec => "iB"
Si => "B"
IecShort | SiShort => ""
}
}
///|
/// Formatting wrapper for byte sizes.
///
/// `Display` stores the raw byte count together with the selected `Format`.
/// Newly created instances default to IEC formatting until you switch styles.
pub struct Display {
byte_size : UInt64
format : Format
} derive(Debug)
///|
pub impl Show for Display with fn to_string(self) {
self.to_string()
}
///|
/// Creates a display wrapper with the default IEC format.
///
/// The provided `byte_size` is stored unchanged. Call `si`, `si_short`, or
/// `iec_short` on the returned value when you want a different rendering style.
pub fn Display::new(byte_size : UInt64) -> Display {
{ byte_size, format: Iec }
}
///|
/// Switches the display wrapper to IEC long format.
///
/// This produces output such as `11.8 MiB`, with a separating space and the
/// binary `iB` suffix.
pub fn Display::iec(self : Display) -> Display {
{ ..self, format: Iec }
}
///|
/// Switches the display wrapper to IEC short format.
///
/// This produces compact output such as `11.8M`. The short form is convenient
/// for narrow UI surfaces and shell output that is sorted with `sort -h`.
pub fn Display::iec_short(self : Display) -> Display {
{ ..self, format: IecShort }
}
///|
/// Switches the display wrapper to SI long format.
///
/// This produces output such as `12.3 MB`, which matches the decimal unit
/// system commonly used by storage devices and transfer rates.
pub fn Display::si(self : Display) -> Display {
{ ..self, format: Si }
}
///|
/// Switches the display wrapper to SI short format.
///
/// This produces compact decimal strings such as `12.3M`, which are useful
/// when you want SI semantics but minimal visual noise.
pub fn Display::si_short(self : Display) -> Display {
{ ..self, format: SiShort }
}
///|
/// Calculates the 1-based unit index using repeated division only.
///
/// This helper mirrors the format-selection logic without relying on logarithms,
/// which makes it useful in restricted environments or for validation tests.
pub fn ideal_unit_no_std(size : Double, unit : UInt64) -> Int {
let mut ideal_prefix = 0
let mut ideal_size = size
let unit_f = unit.to_double()
while true {
ideal_prefix += 1
ideal_size /= unit_f
if ideal_size < unit_f {
break
}
}
ideal_prefix
}
///|
/// Calculates the same unit index as `ideal_unit_no_std` by using logarithms.
fn ideal_unit_std(size : Double, unit_base : Double) -> Int {
let exp = (@math.ln(size) / unit_base).to_int()
if exp == 0 {
1
} else {
exp
}
}
///|
/// Formats the stored byte count as a human-readable string.
///
/// Values smaller than the first unit are rendered as bytes, while larger
/// values are promoted to the most suitable unit for the active `Format`.
/// `precision` controls how many decimal places are kept after rounding.
pub fn Display::to_string(self : Display, precision? : Int = 1) -> String {
let bytes = self.byte_size
let unit = self.format.unit()
let unit_base = self.format.unit_base()
let unit_prefixes = self.format.unit_prefixes()
let unit_separator = self.format.unit_separator()
let unit_suffix = self.format.unit_suffix()
if bytes < unit {
"\{bytes}\{unit_separator}B"
} else {
let size = bytes.to_double()
let exp = ideal_unit_std(size, unit_base)
let unit_prefix = match unit_prefixes.get_char(exp - 1) {
Some(c) => c.to_string()
None => "?"
}
let unit_power = @math.pow(unit.to_double(), exp.to_double())
let value = size / unit_power
// Format the number with specified precision
let formatted_value = format_double_with_precision(value, precision)
"\{formatted_value}\{unit_separator}\{unit_prefix}\{unit_suffix}"
}
}
///|
/// Rounds a floating-point value to the requested number of decimal places.
fn format_double_with_precision(value : Double, precision : Int) -> String {
// Simple implementation - in a real scenario you might want more sophisticated formatting
let multiplier = @math.pow(10.0, precision.to_double())
let rounded = @math.round(value * multiplier) / multiplier
rounded.to_string()
}
///|
test "ideal_unit_selection_std_no_std_iec" {
let bytes = ByteSize::kib(2) // 2048 bytes
if bytes.as_u64() < 1025UL {
return
}
let size = bytes.as_u64().to_double()
let std_result = ideal_unit_std(size, LN_KIB)
let no_std_result = ideal_unit_no_std(size, KIB)
assert_eq(std_result, no_std_result)
}
///|
test "ideal_unit_selection_std_no_std_si" {
let bytes = ByteSize::kb(2) // 2000 bytes
if bytes.as_u64() < 1025UL {
return
}
let size = bytes.as_u64().to_double()
let std_result = ideal_unit_std(size, LN_KB)
let no_std_result = ideal_unit_no_std(size, KB)
assert_eq(std_result, no_std_result)
}
///|
test "to_string_iec" {
let display = Display::{ byte_size: ByteSize::gib(1).as_u64(), format: Iec }
assert_eq("1 GiB", display.to_string())
let display2 = Display::{ byte_size: ByteSize::gb(1).as_u64(), format: Iec }
assert_eq("953.7 MiB", display2.to_string())
}
///|
test "to_string_si" {
let display = Display::{ byte_size: ByteSize::gib(1).as_u64(), format: Si }
assert_eq("1.1 GB", display.to_string())
let display2 = Display::{ byte_size: ByteSize::gb(1).as_u64(), format: Si }
assert_eq("1 GB", display2.to_string())
}
///|
test "to_string_short" {
let display = Display::{
byte_size: ByteSize::gib(1).as_u64(),
format: IecShort,
}
assert_eq("1G", display.to_string())
let display2 = Display::{
byte_size: ByteSize::gb(1).as_u64(),
format: IecShort,
}
assert_eq("953.7M", display2.to_string())
}
///|
test "format_accessors" {
let iec = Format::Iec
let iec_short = Format::IecShort
let si = Format::Si
let si_short = Format::SiShort
assert_eq(iec.unit(), KIB)
assert_eq(si.unit(), KB)
assert_true(iec.unit_base() > 6.9)
assert_true(si.unit_base() > 6.8)
assert_eq(iec.unit_prefixes(), "KMGTPE")
assert_eq(si_short.unit_prefixes(), "kMGTPE")
assert_eq(iec_short.unit_separator(), "")
assert_eq(si.unit_separator(), " ")
assert_eq(iec.unit_suffix(), "iB")
assert_eq(si.unit_suffix(), "B")
assert_eq(si_short.unit_suffix(), "")
}
///|
/// Keeps the display-format assertions concise across the formatting tests.
fn assert_to_string(
expected : String,
byte_size : ByteSize,
format : Format,
) -> Unit raise {
let display = Display::{ byte_size: byte_size.as_u64(), format }
assert_eq(expected, display.to_string())
}
///|
test "test_to_string_as" {
assert_to_string("215 B", ByteSize::b(215), Iec)
assert_to_string("215 B", ByteSize::b(215), Si)
assert_to_string("1 KiB", ByteSize::kib(1), Iec)
assert_to_string("1 kB", ByteSize::kib(1), Si)
assert_to_string("293.9 KiB", ByteSize::kb(301), Iec)
assert_to_string("301 kB", ByteSize::kb(301), Si)
assert_to_string("1 MiB", ByteSize::mib(1), Iec)
assert_to_string("1 MB", ByteSize::mib(1), Si)
assert_to_string("1.9 GiB", ByteSize::mib(1907), Iec)
assert_to_string("2 GB", ByteSize::mib(1908), Si)
assert_to_string("399.6 MiB", ByteSize::mb(419), Iec)
assert_to_string("419 MB", ByteSize::mb(419), Si)
assert_to_string("482.4 GiB", ByteSize::gb(518), Iec)
assert_to_string("518 GB", ByteSize::gb(518), Si)
assert_to_string("741.2 TiB", ByteSize::tb(815), Iec)
assert_to_string("815 TB", ByteSize::tb(815), Si)
assert_to_string("540.9 PiB", ByteSize::pb(609), Iec)
assert_to_string("609 PB", ByteSize::pb(609), Si)
}
///|
test "precision" {
let size = ByteSize::mib(1908)
// Test default precision (1 decimal place)
assert_eq("1.9 GiB", size.to_string())
// Test precision 0
assert_eq("2 GiB", size.display().to_string(precision=0))
// Test precision 5
assert_eq("1.86328 GiB", size.display().to_string(precision=5))
}
///|
test "precision_with_different_formats" {
let size = ByteSize::gb(1)
// Test IEC format with different precisions
assert_eq("953.7 MiB", size.display().iec().to_string(precision=1))
assert_eq("954 MiB", size.display().iec().to_string(precision=0))
assert_eq("953.67432 MiB", size.display().iec().to_string(precision=5))
// Test SI format with different precisions
assert_eq("1 GB", size.display().si().to_string(precision=1))
assert_eq("1 GB", size.display().si().to_string(precision=0))
assert_eq("1 GB", size.display().si().to_string(precision=5))
}
///|
test "short_format_precision" {
let size = ByteSize::mib(1500)
// Test IEC short format
assert_eq("1.5G", size.display().iec_short().to_string(precision=1))
assert_eq("1G", size.display().iec_short().to_string(precision=0))
// Test SI short format
assert_eq("1.6G", size.display().si_short().to_string(precision=1))
assert_eq("2G", size.display().si_short().to_string(precision=0))
}
///|
test "bytes_display" {
// Test that bytes are always displayed as whole numbers regardless of precision
let small_size = ByteSize::b(42)
assert_eq("42 B", small_size.display().iec().to_string(precision=1))
assert_eq("42 B", small_size.display().iec().to_string(precision=0))
assert_eq("42 B", small_size.display().iec().to_string(precision=5))
assert_eq("42 B", small_size.display().si().to_string(precision=1))
assert_eq("42 B", small_size.display().si().to_string(precision=0))
assert_eq("42 B", small_size.display().si().to_string(precision=5))
}
///|
test "large_values_precision" {
let large_size = ByteSize::pib(2)
assert_eq("2 PiB", large_size.display().iec().to_string(precision=1))
assert_eq("2 PiB", large_size.display().iec().to_string(precision=0))
// Test corresponding SI display
assert_eq("2.3 PB", large_size.display().si().to_string(precision=1))
assert_eq("2 PB", large_size.display().si().to_string(precision=0))
}