#![doc(html_root_url = "https://docs.rs/human_format")]
//! `human_format` provides facilitates creating a formatted string, converting between numbers that are beyond typical
//! needs for humans into a simpler string that conveys the gist of the meaning of the number.
//!
//! ## Setup
//!
//! Add the library to your dependencies listing
//!
//! ```toml
//! [dependencies]
//! human_format = "0.2"
//! ```
//!
//! Add the crate reference at your crate root
//!
//! ```rust
//! extern crate human_format;
//! ```
//!
//! Print some human readable strings
//!
//! ```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 lib. Use this to handle your formatting needs.
#[derive(Debug)]
pub struct Formatter {
decimals: usize,
separator: String,
scales: Scales,
forced_units: String,
forced_suffix: String,
}
impl Default for Formatter {
fn default() -> Self {
Formatter {
decimals: 2,
separator: " ".to_owned(),
scales: Scales::SI(),
forced_units: "".to_owned(),
forced_suffix: "".to_owned(),
}
}
}
/// Provide a customized scaling scheme for your own modeling.
#[derive(Debug)]
pub struct Scales {
base: u32,
suffixes: Vec,
}
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
}
/// Formats the number into a string
pub fn format(&self, value: f64) -> String {
if value < 0.0 {
return format!("-{}", self.format(value * -1.0));
}
let scaled_value = self.scales.to_scaled_value(value);
format!(
"{:.width$}{}{}{}",
scaled_value.value,
self.separator,
scaled_value.suffix,
self.forced_units,
width = self.decimals
)
}
/// Parse a string back into a float value.
pub fn parse(&self, value: &str) -> f64 {
let v: Vec<&str> = value.split(&self.separator).collect();
let result = v.first().unwrap().parse::().unwrap();
let mut suffix = v.get(1).unwrap().to_string();
let new_len = suffix.len() - self.forced_units.len();
suffix.truncate(new_len);
let magnitude_multiplier = self.scales.get_magnitude_multiplier(&suffix);
result * magnitude_multiplier
}
/// Attempt to parse a string back into a float value.
pub fn try_parse(&self, value: &str) -> Result {
// Remove suffix if present
let value = value.trim_end_matches(&self.forced_units).to_string();
// Find Suffix
let mut number = String::new();
for c in value.chars() {
if c.is_digit(10) || c == '.' {
number.push(c);
} else {
break;
}
}
let suffix = value
.trim_start_matches(&number)
.trim_start_matches(&self.separator)
.to_string();
let number = number.parse::().map_err(|e| e.to_string())?;
let magnitude_multiplier = self.scales.try_get_magnitude_multiplier(&suffix)?;
Ok(number * magnitude_multiplier)
}
}
impl Default for Scales {
fn default() -> Self {
Scales::SI()
}
}
impl Scales {
/// Instantiates a new `Scales` with SI keys
pub fn new() -> Self {
Scales::SI()
}
/// 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(),
],
}
}
/// 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(),
],
}
}
/// 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 {
self.suffixes
.iter()
.enumerate()
.find_map(|(idx, x)| {
if value == x {
Some((self.base as f64).powi(idx as i32))
} else {
None
}
})
.ok_or_else(|| {
format!(
"Unknown suffix: {value}, valid suffixes are: {}",
self.suffixes
.iter()
.filter(|x| !x.trim().is_empty())
.map(String::to_string)
.collect::>()
.join(", ")
)
})
}
fn get_magnitude_multiplier(&self, value: &str) -> f64 {
for ndx in 0..self.suffixes.len() {
if value == self.suffixes[ndx] {
return (self.base as f64).powi(ndx as i32);
}
}
0.0
}
fn to_scaled_value(&self, value: f64) -> ScaledValue {
let mut index: usize = 0;
let base: f64 = self.base as f64;
let mut value = value;
loop {
if value < base {
break;
}
value /= base;
index += 1;
}
ScaledValue {
value,
suffix: self.suffixes[index].to_owned(),
}
}
}