#![doc(html_root_url = "https://docs.rs/human_format")]
//! `human_format` is a library for formatting numbers into human readable strings. It supports SI, Time and Binary scales out of the box, and allows for custom scales as well.
//!
//! ## Setup
//!
//! Add the library to your dependencies listing
//!
//! ```bash
//! $ cargo add human_format
//! ```
//!
//! ## Usage
//!
//! Print human readable strings from numbers using SI scales by default
//!
//! ```rust
//! // "1.00 k"
//! let tmpStr = human_format::Formatter::new()
//! .format(1000.0);
//! # assert_eq!(tmpStr, "1.00 k");
//!
//! // "1.00 M"
//! let tmpStr2 = human_format::Formatter::new()
//! .format(1000000.0);
//! # assert_eq!(tmpStr2, "1.00 M");
//!
//! // "1.00 G"
//! let tmpStr3 = human_format::Formatter::new()
//! .format(1000000000.0);
//! # assert_eq!(tmpStr3, "1.00 G");
//! ```
//!
//! If you are so inspired you can even try playing with units and customizing your `Scales`
//!
//! For more examples you should review the examples on github: [tests/demo.rs](https://github.com/BobGneu/human-format-rs/blob/master/tests/demo.rs)
//!
#[derive(Debug)]
struct ScaledValue {
value: f64,
suffix: String,
}
/// Entry point to the library. Use this to handle your formatting needs.
#[derive(Debug)]
pub struct Formatter {
decimals: usize,
separator: String,
scales: Scales,
forced_units: String,
forced_suffix: String,
use_micro_sign: bool,
}
impl Default for Formatter {
fn default() -> Self {
Formatter {
decimals: 2,
separator: " ".to_owned(),
scales: Scales::new(),
forced_units: "".to_owned(),
forced_suffix: "".to_owned(),
use_micro_sign: false,
}
}
}
/// Provide a customized scaling scheme for your own modeling.
#[derive(Debug)]
pub struct Scales {
base: u32,
suffixes: Vec,
suffixes_neg: Vec,
explicit_map: Option>,
}
impl Formatter {
/// Initializes a new `Formatter` with default values.
pub fn new() -> Self {
Default::default()
}
/// Sets the decimals value for formatting the string.
pub fn with_decimals(&mut self, decimals: usize) -> &mut Self {
self.decimals = decimals;
self
}
/// Sets the separator value for formatting the string.
pub fn with_separator(&mut self, separator: &str) -> &mut Self {
self.separator = separator.to_owned();
self
}
/// Sets the scales value.
pub fn with_scales(&mut self, scales: Scales) -> &mut Self {
self.scales = scales;
self
}
/// Sets the units value.
pub fn with_units(&mut self, units: &str) -> &mut Self {
self.forced_units = units.to_owned();
self
}
/// Sets the expected suffix value.
pub fn with_suffix(&mut self, suffix: &str) -> &mut Self {
self.forced_suffix = suffix.to_owned();
self
}
/// Enable using the micro sign `µ` in formatted output when fractional suffix is `u`.
pub fn with_micro_sign(&mut self, enable: bool) -> &mut Self {
self.use_micro_sign = enable;
self
}
/// Formats the number into a string
pub fn format(&self, value: f64) -> String {
// Handle non-finite values explicitly to avoid loops in scaling logic
if value.is_nan() {
return "NaN".to_owned();
}
if value < 0.0 {
return format!("-{}", self.format(-value));
}
if value.is_infinite() {
return "inf".to_owned();
}
// If a forced suffix is provided, attempt to scale to that suffix
let scaled_value = if !self.forced_suffix.is_empty() {
// normalize micro sign in forced suffix when looking up
let lookup = self.forced_suffix.replace('\u{00B5}', "u");
match self.scales.try_get_magnitude_multiplier(&lookup) {
Ok(mult) => ScaledValue {
value: value / mult,
suffix: self.forced_suffix.clone(),
},
Err(_) => self.scales.to_scaled_value(value),
}
} else {
self.scales.to_scaled_value(value)
};
let out_suffix = if self.use_micro_sign && scaled_value.suffix == "u" {
"µ".to_owned()
} else {
scaled_value.suffix.clone()
};
format!(
"{:.width$}{}{}{}",
scaled_value.value,
self.separator,
out_suffix,
self.forced_units,
width = self.decimals
)
}
/// Parse a string back into a float value.
///
/// This convenience wrapper unwraps the result of `try_parse` and will panic
/// on malformed input. It is feature-gated behind `panic_parse` so that
/// consumers must opt-in to the panicking API via the panic_parse feature.
#[cfg(feature = "panic_parse")]
#[deprecated(
note = "Use `try_parse`, which returns `Result` and does not panic on malformed input"
)]
pub fn parse(&self, value: &str) -> f64 {
self.try_parse(value).unwrap()
}
/// Attempt to parse a string back into a float value.
///
/// Examples:
///
/// ```rust
/// use human_format::{Formatter, Scales};
/// // SI example
/// let f = Formatter::new();
/// assert_eq!(f.try_parse("1.00 k").unwrap(), 1000.0);
/// // Binary scales (ki = 1024)
/// let mut fbin = Formatter::new();
/// fbin.with_scales(Scales::Binary());
/// assert_eq!(fbin.try_parse("1.00 ki").unwrap(), 1024.0);
/// // Units specified via with_units() are automatically stripped from input
/// let mut funit = Formatter::new();
/// funit.with_units("B");
/// assert_eq!(funit.try_parse("1.00 kB").unwrap(), 1000.0);
/// // Negative numbers
/// assert_eq!(Formatter::new().try_parse("-1.0 k").unwrap(), -1000.0);
/// // Invalid input
/// assert!(Formatter::new().try_parse("bad input").is_err());
/// ```
pub fn try_parse(&self, value: &str) -> Result {
let (number_str, suffix) = self.parse_components(value)?;
let number = number_str
.parse::()
.map_err(ParseError::InvalidNumber)?;
let magnitude_multiplier = self.scales.try_get_magnitude_multiplier(&suffix)?;
Ok(number * magnitude_multiplier)
}
fn parse_components(&self, value: &str) -> Result<(String, String), ParseError> {
// Remove forced units if present
let value = value
.trim()
.trim_end_matches(&self.forced_units)
.to_string();
// Extract leading number (allow sign and decimal)
let mut number = String::new();
for (i, c) in value.chars().enumerate() {
if c.is_ascii_digit() || c == '.' || (c == '-' && i == 0) {
number.push(c);
} else {
break;
}
}
if number.is_empty() {
return Err(ParseError::EmptyInput);
}
let suffix = value
.trim_start_matches(&number)
.trim_start_matches(&self.separator)
.to_string();
// Normalize common variants: acceptance of micro sign 'µ'
let suffix = suffix.replace('\u{00B5}', "u");
Ok((number, suffix))
}
/// Parse a string and optionally clamp unknown suffixes to the largest suffix multiplier.
///
/// If `clamp` is `false`, this behaves like `try_parse` and returns an error on unknown suffixes.
/// If `clamp` is `true`, unknown suffixes will be interpreted as the largest available suffix.
///
/// Examples:
///
/// ```rust
/// use human_format::{Formatter, Scales};
/// let f = Formatter::new();
/// // Unknown suffix errors when clamp == false
/// assert!(f.parse_or_clamp("1.0 DN", false).is_err());
/// // Unknown suffix clamps to largest suffix multiplier when clamp == true
/// assert!(f.parse_or_clamp("1.0 DN", true).is_ok());
/// // Binary example with units
/// let mut fb = Formatter::new();
/// fb.with_scales(Scales::Binary()).with_units("B");
/// assert_eq!(fb.parse_or_clamp("1.0 kiB", false).unwrap(), 1024.0);
/// // Negative number with clamp
/// assert_eq!(Formatter::new().parse_or_clamp("-1.0 k", true).unwrap(), -1000.0);
/// ```
pub fn parse_or_clamp(&self, value: &str, clamp: bool) -> Result {
let (number_str, suffix) = self.parse_components(value)?;
let number = number_str
.parse::()
.map_err(ParseError::InvalidNumber)?;
match self.scales.try_get_magnitude_multiplier(&suffix) {
Ok(mult) => Ok(number * mult),
Err(ParseError::UnknownSuffix(_)) if clamp => {
// If scales has an explicit_map (e.g., Time), clamp to the
// largest explicit multiplier rather than assuming a power of
// `base` matching the last suffix index.
if let Some(map) = &self.scales.explicit_map
&& !map.is_empty()
{
let max_mult = map.values().copied().fold(f64::NEG_INFINITY, f64::max);
return Ok(number * max_mult);
}
let last_index = self.scales.suffixes.len().saturating_sub(1);
let mult = (self.scales.base as f64).powi(last_index as i32);
Ok(number * mult)
}
Err(e) => Err(e),
}
}
}
/// Errors returned by parsing operations.
#[derive(Debug)]
pub enum ParseError {
EmptyInput,
InvalidNumber(std::num::ParseFloatError),
UnknownSuffix(String),
}
impl PartialEq for ParseError {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(ParseError::EmptyInput, ParseError::EmptyInput) => true,
// Note: `ParseFloatError` does not implement `PartialEq` on stable Rust.
// We therefore treat all `InvalidNumber(_)` variants as equal to each other,
// ignoring the inner `ParseFloatError`.
(ParseError::InvalidNumber(_), ParseError::InvalidNumber(_)) => true,
(ParseError::UnknownSuffix(a), ParseError::UnknownSuffix(b)) => a == b,
_ => false,
}
}
}
impl std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParseError::EmptyInput => write!(f, "Empty input"),
ParseError::InvalidNumber(e) => write!(f, "Invalid number: {}", e),
ParseError::UnknownSuffix(s) => write!(f, "Unknown suffix: {}", s),
}
}
}
impl std::error::Error for ParseError {}
impl Default for Scales {
fn default() -> Self {
Scales::SI()
}
}
impl Scales {
/// Instantiates a new `Scales` with SI keys
pub fn new() -> Self {
Default::default()
}
/// Instantiates a new `Scales` with SI keys
#[allow(non_snake_case)]
pub fn SI() -> Self {
Scales {
base: 1000,
suffixes: vec![
"".to_owned(),
"k".to_owned(),
"M".to_owned(),
"G".to_owned(),
"T".to_owned(),
"P".to_owned(),
"E".to_owned(),
"Z".to_owned(),
"Y".to_owned(),
"R".to_owned(),
"Q".to_owned(),
],
suffixes_neg: vec![
"".to_owned(),
"m".to_owned(), // milli
"u".to_owned(), // micro (use 'u' ascii for micro)
"n".to_owned(), // nano
"p".to_owned(), // pico
"f".to_owned(), // femto
"a".to_owned(), // atto
"z".to_owned(), // zepto
"y".to_owned(), // yocto
"r".to_owned(), // ronto (10^-27)
"q".to_owned(), // quecto (10^-30)
],
explicit_map: None,
}
}
/// Instantiates a new `Scales` with Binary keys
#[allow(non_snake_case)]
pub fn Binary() -> Self {
Scales {
base: 1024,
suffixes: vec![
"".to_owned(),
"ki".to_owned(),
"Mi".to_owned(),
"Gi".to_owned(),
"Ti".to_owned(),
"Pi".to_owned(),
"Ei".to_owned(),
"Zi".to_owned(),
"Yi".to_owned(),
"Ri".to_owned(),
"Qi".to_owned(),
],
// binary scales usually don't define fractional SI-like prefixes; keep empty placeholder
suffixes_neg: vec!["".to_owned()],
explicit_map: None,
}
}
/// Instantiates a new `Scales` for time units.
///
/// This maps common time suffixes to multipliers in seconds.
#[allow(non_snake_case)]
pub fn Time() -> Self {
use std::collections::HashMap;
let mut map: HashMap = HashMap::new();
map.insert("ns".to_owned(), 1e-9);
map.insert("us".to_owned(), 1e-6);
map.insert("ms".to_owned(), 1e-3);
map.insert("s".to_owned(), 1.0);
map.insert("m".to_owned(), 60.0);
map.insert("h".to_owned(), 3600.0);
map.insert("d".to_owned(), 86400.0);
map.insert("w".to_owned(), 604800.0);
// longer period units using average definitions
let year_secs = 365.2425 * 86400.0; // average Gregorian year
let month_secs = year_secs / 12.0; // average month
map.insert("mo".to_owned(), month_secs);
map.insert("month".to_owned(), month_secs);
// quarters: three-month periods
map.insert("qtr".to_owned(), 3.0 * month_secs);
map.insert("y".to_owned(), year_secs);
map.insert("yr".to_owned(), year_secs);
map.insert("year".to_owned(), year_secs);
map.insert("dec".to_owned(), 10.0 * year_secs);
map.insert("decade".to_owned(), 10.0 * year_secs);
map.insert("c".to_owned(), 100.0 * year_secs);
map.insert("century".to_owned(), 100.0 * year_secs);
map.insert("kyr".to_owned(), 1000.0 * year_secs); // millennium (kilo-year)
map.insert("millennium".to_owned(), 1000.0 * year_secs);
map.insert("Myr".to_owned(), 1.0e6 * year_secs);
map.insert("Gyr".to_owned(), 1.0e9 * year_secs);
Scales {
base: 1,
suffixes: vec![],
suffixes_neg: vec![],
explicit_map: Some(map),
}
}
/// Sets the base for the `Scales`
pub fn with_base(&mut self, base: u32) -> &mut Self {
self.base = base;
self
}
/// Sets the suffixes listing appropriately
pub fn with_suffixes(&mut self, suffixes: Vec<&str>) -> &mut Self {
self.suffixes = Vec::new();
for suffix in suffixes {
// This should be to_owned to be clear about intent.
// https://users.rust-lang.org/t/to-string-vs-to-owned-for-string-literals/1441/6
self.suffixes.push(suffix.to_owned());
}
self
}
fn try_get_magnitude_multiplier(&self, value: &str) -> Result {
// If an explicit mapping exists (e.g., time units), prefer it
if let Some(map) = &self.explicit_map
&& let Some(val) = map.get(value)
{
return Ok(*val);
}
// positive suffixes
if let Some((idx, _)) = self.suffixes.iter().enumerate().find(|(_, x)| x == &value) {
return Ok((self.base as f64).powi(idx as i32));
}
// negative suffixes (fractions)
if let Some((idx, _)) = self
.suffixes_neg
.iter()
.enumerate()
.find(|(_, x)| x == &value)
{
// idx 0 corresponds to base^0 (no scaling); idx 1 => base^-1, idx 2 => base^-2
let exp = -(idx as i32);
return Ok((self.base as f64).powi(exp));
}
// build valid suffix list for error message
let mut valid: Vec = Vec::new();
if let Some(map) = &self.explicit_map {
valid.extend(map.keys().cloned());
}
valid.extend(
self.suffixes
.iter()
.filter(|x| !x.trim().is_empty())
.cloned(),
);
valid.extend(
self.suffixes_neg
.iter()
.filter(|x| !x.trim().is_empty())
.cloned(),
);
Err(ParseError::UnknownSuffix(format!(
"{}; valid suffixes are: {}",
value,
valid.join(", ")
)))
}
fn to_scaled_value(&self, value: f64) -> ScaledValue {
let mut index: usize = 0;
let base: f64 = self.base as f64;
let mut value = value;
// Prevent infinite loops for non-finite values and cap index to available suffixes
let last_index = self.suffixes.len().saturating_sub(1);
let last_neg = self.suffixes_neg.len().saturating_sub(1);
// If explicit map provided (e.g., Time), prefer selecting suffix from it
if let Some(map) = &self.explicit_map {
// Build vector of (suffix, multiplier) and sort descending by multiplier
let mut entries: Vec<(String, f64)> =
map.iter().map(|(k, v)| (k.clone(), *v)).collect();
entries.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
if value > 0.0 {
for (suf, mult) in entries.iter() {
if value >= *mult {
return ScaledValue {
value: value / *mult,
suffix: suf.clone(),
};
}
}
// If smaller than smallest multiplier, use smallest (e.g., ns)
if let Some((suf, mult)) = entries.last() {
return ScaledValue {
value: value / *mult,
suffix: suf.clone(),
};
}
}
}
if value >= base {
while value >= base && index < last_index {
value /= base;
index += 1;
}
ScaledValue {
value,
suffix: self.suffixes[index].to_owned(),
}
} else if value > 0.0 && value < 1.0 {
// Use negative prefixes for fractional values
let mut neg_idx: usize = 0;
while value < 1.0 && neg_idx < last_neg {
value *= base;
neg_idx += 1;
}
ScaledValue {
value,
suffix: self.suffixes_neg[neg_idx].to_owned(),
}
} else {
ScaledValue {
value,
suffix: self.suffixes[0].to_owned(),
}
}
}
}