#![doc(html_root_url = "https://docs.rs/human_format")]
#[derive(Debug)]
struct ScaledValue {
value: f64,
suffix: String,
}
#[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,
}
}
}
#[derive(Debug)]
pub struct Scales {
base: u32,
suffixes: Vec<String>,
suffixes_neg: Vec<String>,
explicit_map: Option<std::collections::HashMap<String, f64>>,
}
impl Formatter {
pub fn new() -> Self {
Default::default()
}
pub fn with_decimals(&mut self, decimals: usize) -> &mut Self {
self.decimals = decimals;
self
}
pub fn with_separator(&mut self, separator: &str) -> &mut Self {
self.separator = separator.to_owned();
self
}
pub fn with_scales(&mut self, scales: Scales) -> &mut Self {
self.scales = scales;
self
}
pub fn with_units(&mut self, units: &str) -> &mut Self {
self.forced_units = units.to_owned();
self
}
pub fn with_suffix(&mut self, suffix: &str) -> &mut Self {
self.forced_suffix = suffix.to_owned();
self
}
pub fn with_micro_sign(&mut self, enable: bool) -> &mut Self {
self.use_micro_sign = enable;
self
}
pub fn format(&self, value: f64) -> String {
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();
}
let scaled_value = if !self.forced_suffix.is_empty() {
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
)
}
#[cfg(feature = "panic_parse")]
#[deprecated(
note = "Use `try_parse`, which returns `Result<f64, ParseError>` and does not panic on malformed input"
)]
pub fn parse(&self, value: &str) -> f64 {
self.try_parse(value).unwrap()
}
pub fn try_parse(&self, value: &str) -> Result<f64, ParseError> {
let (number_str, suffix) = self.parse_components(value)?;
let number = number_str
.parse::<f64>()
.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> {
let value = value
.trim()
.trim_end_matches(&self.forced_units)
.to_string();
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();
let suffix = suffix.replace('\u{00B5}', "u");
Ok((number, suffix))
}
pub fn parse_or_clamp(&self, value: &str, clamp: bool) -> Result<f64, ParseError> {
let (number_str, suffix) = self.parse_components(value)?;
let number = number_str
.parse::<f64>()
.map_err(ParseError::InvalidNumber)?;
match self.scales.try_get_magnitude_multiplier(&suffix) {
Ok(mult) => Ok(number * mult),
Err(ParseError::UnknownSuffix(_)) if clamp => {
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),
}
}
}
#[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,
(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 {
pub fn new() -> Self {
Default::default()
}
#[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(), "u".to_owned(), "n".to_owned(), "p".to_owned(), "f".to_owned(), "a".to_owned(), "z".to_owned(), "y".to_owned(), "r".to_owned(), "q".to_owned(), ],
explicit_map: None,
}
}
#[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(),
],
suffixes_neg: vec!["".to_owned()],
explicit_map: None,
}
}
#[allow(non_snake_case)]
pub fn Time() -> Self {
use std::collections::HashMap;
let mut map: HashMap<String, f64> = 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);
let year_secs = 365.2425 * 86400.0; let month_secs = year_secs / 12.0;
map.insert("mo".to_owned(), month_secs);
map.insert("month".to_owned(), month_secs);
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); 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),
}
}
pub fn with_base(&mut self, base: u32) -> &mut Self {
self.base = base;
self
}
pub fn with_suffixes(&mut self, suffixes: Vec<&str>) -> &mut Self {
self.suffixes = Vec::new();
for suffix in suffixes {
self.suffixes.push(suffix.to_owned());
}
self
}
fn try_get_magnitude_multiplier(&self, value: &str) -> Result<f64, ParseError> {
if let Some(map) = &self.explicit_map
&& let Some(val) = map.get(value)
{
return Ok(*val);
}
if let Some((idx, _)) = self.suffixes.iter().enumerate().find(|(_, x)| x == &value) {
return Ok((self.base as f64).powi(idx as i32));
}
if let Some((idx, _)) = self
.suffixes_neg
.iter()
.enumerate()
.find(|(_, x)| x == &value)
{
let exp = -(idx as i32);
return Ok((self.base as f64).powi(exp));
}
let mut valid: Vec<String> = 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;
let last_index = self.suffixes.len().saturating_sub(1);
let last_neg = self.suffixes_neg.len().saturating_sub(1);
if let Some(map) = &self.explicit_map {
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 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 {
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(),
}
}
}
}