use std::fmt;
use std::str::FromStr;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum BytesError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Invalid unit: {0}")]
InvalidUnit(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ByteUnit {
B,
KB,
MB,
GB,
TB,
PB,
}
impl fmt::Display for ByteUnit {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ByteUnit::B => write!(f, "B"),
ByteUnit::KB => write!(f, "KB"),
ByteUnit::MB => write!(f, "MB"),
ByteUnit::GB => write!(f, "GB"),
ByteUnit::TB => write!(f, "TB"),
ByteUnit::PB => write!(f, "PB"),
}
}
}
impl FromStr for ByteUnit {
type Err = BytesError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"b" => Ok(ByteUnit::B),
"kb" => Ok(ByteUnit::KB),
"mb" => Ok(ByteUnit::MB),
"gb" => Ok(ByteUnit::GB),
"tb" => Ok(ByteUnit::TB),
"pb" => Ok(ByteUnit::PB),
_ => Err(BytesError::InvalidUnit(s.to_string())),
}
}
}
impl ByteUnit {
pub fn multiplier(&self) -> u64 {
match self {
ByteUnit::B => 1,
ByteUnit::KB => 1 << 10,
ByteUnit::MB => 1 << 20,
ByteUnit::GB => 1 << 30,
ByteUnit::TB => 1u64 << 40,
ByteUnit::PB => 1u64 << 50,
}
}
}
#[derive(Debug, Clone)]
pub struct BytesOptions {
pub unit: Option<ByteUnit>,
pub decimal_places: usize,
pub fixed_decimals: bool,
pub thousands_separator: String,
pub unit_separator: String,
}
impl Default for BytesOptions {
fn default() -> Self {
Self {
unit: None,
decimal_places: 2,
fixed_decimals: false,
thousands_separator: String::new(),
unit_separator: String::new(),
}
}
}
pub struct Bytes;
impl Bytes {
pub fn new() -> Self {
Self
}
pub fn convert_number(
&self,
value: u64,
options: Option<BytesOptions>,
) -> Result<String, BytesError> {
self.format(value, options)
}
pub fn convert_string(&self, value: &str) -> Result<u64, BytesError> {
self.parse(value)
}
pub fn parse(&self, val: &str) -> Result<u64, BytesError> {
let val = val.trim();
if let Ok(num) = val.parse::<f64>() {
if num < 0.0 {
return Err(BytesError::ParseError(
"Negative values not allowed".to_string(),
));
}
return Ok(num.floor() as u64);
}
let re = regex::Regex::new(r"^([-+]?\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb|pb)?$")
.map_err(|e| BytesError::ParseError(format!("Regex error: {e}")))?;
if let Some(captures) = re.captures(&val.to_lowercase()) {
let number_str = captures.get(1).unwrap().as_str();
let unit_str = captures.get(2).map(|m| m.as_str()).unwrap_or("b");
let float_value: f64 = number_str
.parse()
.map_err(|_| BytesError::ParseError(format!("Invalid number: {number_str}")))?;
if float_value < 0.0 {
return Err(BytesError::ParseError(
"Negative values not allowed".to_string(),
));
}
let unit: ByteUnit = unit_str.parse()?;
let multiplier = unit.multiplier();
Ok((float_value * multiplier as f64).floor() as u64)
} else {
Err(BytesError::ParseError(format!("Invalid format: {val}")))
}
}
pub fn format(&self, value: u64, options: Option<BytesOptions>) -> Result<String, BytesError> {
let options = options.unwrap_or_default();
let num = value as f64;
let unit = if let Some(unit) = options.unit {
unit
} else {
if num >= ByteUnit::PB.multiplier() as f64 {
ByteUnit::PB
} else if num >= ByteUnit::TB.multiplier() as f64 {
ByteUnit::TB
} else if num >= ByteUnit::GB.multiplier() as f64 {
ByteUnit::GB
} else if num >= ByteUnit::MB.multiplier() as f64 {
ByteUnit::MB
} else if num >= ByteUnit::KB.multiplier() as f64 {
ByteUnit::KB
} else {
ByteUnit::B
}
};
let val = num / unit.multiplier() as f64;
let mut num_str = format!("{:.prec$}", val, prec = options.decimal_places);
if !options.fixed_decimals {
if num_str.contains('.') {
num_str = num_str.trim_end_matches('0').trim_end_matches('.').to_string();
}
}
if !options.thousands_separator.is_empty() {
num_str = self.add_thousands_separator(&num_str, &options.thousands_separator);
}
Ok(format!("{}{}{}", num_str, options.unit_separator, unit))
}
fn add_thousands_separator(&self, num_str: &str, separator: &str) -> String {
let parts: Vec<&str> = num_str.split('.').collect();
let integer_part = parts[0];
let decimal_part = if parts.len() > 1 {
Some(parts[1])
} else {
None
};
let mut result = String::new();
let chars: Vec<char> = integer_part.chars().rev().collect();
for (i, ch) in chars.iter().enumerate() {
if i > 0 && i % 3 == 0 {
result.push_str(separator);
}
result.push(*ch);
}
let integer_result: String = result.chars().rev().collect();
if let Some(decimal) = decimal_part {
format!("{integer_result}.{decimal}")
} else {
integer_result
}
}
}
impl Default for Bytes {
fn default() -> Self {
Self::new()
}
}
static BYTES_INSTANCE: std::sync::OnceLock<Bytes> = std::sync::OnceLock::new();
fn get_bytes_instance() -> &'static Bytes {
BYTES_INSTANCE.get_or_init(Bytes::new)
}
pub fn bytes(value: u64) -> Result<String, BytesError> {
get_bytes_instance().convert_number(value, None)
}
pub fn parse_bytes(value: &str) -> Result<u64, BytesError> {
get_bytes_instance().convert_string(value)
}