use alloc::format;
use alloc::string::String;
use crate::number::Number;
const COMPACT: [&str; 5] = ["", "K", "M", "B", "T"];
const MAX_PRECISION: usize = u16::MAX as usize;
const WORD: [&str; 12] = [
"",
"thousand",
"million",
"billion",
"trillion",
"quadrillion",
"quintillion",
"sextillion",
"septillion",
"octillion",
"nonillion",
"decillion",
];
const METRIC: [&str; 11] = ["", "k", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q"];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Notation {
#[default]
Compact,
Word,
Metric,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Formatter {
notation: Notation,
precision: usize,
trim_zeros: bool,
space: bool,
sign_plus: bool,
}
impl Default for Formatter {
fn default() -> Self {
Self::compact()
}
}
impl Formatter {
#[must_use]
pub const fn compact() -> Self {
Self {
notation: Notation::Compact,
precision: 1,
trim_zeros: true,
space: false,
sign_plus: false,
}
}
#[must_use]
pub const fn word() -> Self {
Self {
notation: Notation::Word,
precision: 1,
trim_zeros: true,
space: true,
sign_plus: false,
}
}
#[must_use]
pub const fn metric() -> Self {
Self {
notation: Notation::Metric,
precision: 1,
trim_zeros: true,
space: true,
sign_plus: false,
}
}
#[must_use]
pub const fn new(notation: Notation) -> Self {
match notation {
Notation::Compact => Self::compact(),
Notation::Word => Self::word(),
Notation::Metric => Self::metric(),
}
}
#[must_use]
pub const fn precision(mut self, precision: usize) -> Self {
self.precision = precision;
self
}
#[must_use]
pub const fn trim_zeros(mut self, trim: bool) -> Self {
self.trim_zeros = trim;
self
}
#[must_use]
pub const fn space(mut self, space: bool) -> Self {
self.space = space;
self
}
#[must_use]
pub const fn sign_plus(mut self, sign_plus: bool) -> Self {
self.sign_plus = sign_plus;
self
}
#[must_use]
pub fn format<N: Number>(&self, value: N) -> String {
self.format_f64(value.to_f64())
}
const fn suffixes(self) -> &'static [&'static str] {
match self.notation {
Notation::Compact => &COMPACT,
Notation::Word => &WORD,
Notation::Metric => &METRIC,
}
}
fn format_f64(&self, value: f64) -> String {
if value.is_nan() {
return String::from("NaN");
}
if value.is_infinite() {
return String::from(if value.is_sign_negative() {
"-inf"
} else {
"inf"
});
}
let negative = value.is_sign_negative();
let magnitude = if negative { -value } else { value };
let precision = self.precision.min(MAX_PRECISION);
let suffixes = self.suffixes();
let max_idx = suffixes.len() - 1;
let mut v = magnitude;
let mut idx = 0;
while v >= 1000.0 && idx < max_idx {
v /= 1000.0;
idx += 1;
}
if idx == max_idx && v >= 1000.0 {
return self.with_sign(negative, false, &format!("{magnitude:e}"));
}
let mut rendered = format!("{v:.precision$}");
if idx < max_idx {
if let Ok(parsed) = rendered.parse::<f64>() {
if parsed >= 1000.0 {
idx += 1;
v /= 1000.0;
rendered = format!("{v:.precision$}");
}
}
}
if self.trim_zeros {
rendered = trim_trailing_zeros(&rendered);
}
let suffix = suffixes[idx];
let body = if suffix.is_empty() {
rendered
} else {
let sep = if self.space { " " } else { "" };
let mut b = rendered;
b.push_str(sep);
b.push_str(suffix);
b
};
self.with_sign(negative, is_all_zero(&body), &body)
}
fn with_sign(&self, negative: bool, is_zero: bool, body: &str) -> String {
let mut out = String::new();
if is_zero {
} else if negative {
out.push('-');
} else if self.sign_plus {
out.push('+');
}
out.push_str(body);
out
}
}
fn is_all_zero(s: &str) -> bool {
s.bytes().all(|b| b == b'0' || b == b'.')
}
fn trim_trailing_zeros(s: &str) -> String {
if s.contains('.') {
String::from(s.trim_end_matches('0').trim_end_matches('.'))
} else {
String::from(s)
}
}