#[derive(Debug, Clone, PartialEq)]
pub struct NumberFormat {
pub style: Style,
pub precision: usize,
pub sci_precision: Option<usize>,
pub fixed_zerofill: bool,
pub sci_zerofill: bool,
pub dec_sep: String,
pub thousands_sep: String,
pub min_exponent_1000: i32,
pub showpos: bool,
pub skip_zero: bool,
pub sci_style: SciStyle,
pub unit_mantissa: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Style {
Std {
lower: Option<i32>,
upper: i32,
},
Fixed,
Sci,
IntDetect,
IntTrunc,
Relative(i32),
FixedRelative,
Frac,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SciStyle {
TenE,
LowerE,
UpperE,
Subscript,
Superscript,
}
impl Default for NumberFormat {
fn default() -> Self {
NumberFormat {
style: Style::Std { lower: None, upper: 4 },
precision: 2,
sci_precision: None,
fixed_zerofill: false,
sci_zerofill: false,
dec_sep: ".".into(),
thousands_sep: "{,}".into(),
min_exponent_1000: 0,
showpos: false,
skip_zero: false,
sci_style: SciStyle::TenE,
unit_mantissa: true,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct Decimal {
negative: bool,
digits: Vec<u8>,
exponent: i32,
}
impl Decimal {
fn parse(v: f64) -> Decimal {
let negative = v < 0.0;
if v == 0.0 {
return Decimal { negative: false, digits: Vec::new(), exponent: 0 };
}
let s = format!("{:e}", v.abs());
let (mantissa, exp) = s.split_once('e').unwrap_or((&s, "0"));
let exponent: i32 = exp.parse().unwrap_or(0);
let digits: Vec<u8> = mantissa.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
let mut d = Decimal { negative, digits, exponent };
d.trim();
d
}
fn trim(&mut self) {
while self.digits.last() == Some(&0) {
self.digits.pop();
}
while self.digits.first() == Some(&0) {
self.digits.remove(0);
self.exponent -= 1;
}
if self.digits.is_empty() {
self.exponent = 0;
}
}
fn is_zero(&self) -> bool {
self.digits.is_empty()
}
fn round_at(&mut self, place: i32) {
if self.is_zero() {
return;
}
let keep = self.exponent - place + 1;
if keep < 0 {
self.digits.clear();
self.trim();
return;
}
let keep = keep as usize;
if keep >= self.digits.len() {
return;
}
let up = self.digits[keep] >= 5;
self.digits.truncate(keep);
if up {
let mut i = keep;
loop {
if i == 0 {
self.digits.insert(0, 1);
self.exponent += 1;
break;
}
i -= 1;
if self.digits[i] == 9 {
self.digits[i] = 0;
} else {
self.digits[i] += 1;
break;
}
}
}
self.trim();
}
fn split(&self) -> (String, String) {
if self.is_zero() {
return ("0".into(), String::new());
}
let mut int = String::new();
let mut frac = String::new();
if self.exponent < 0 {
int.push('0');
for _ in 0..(-self.exponent - 1) {
frac.push('0');
}
for d in &self.digits {
frac.push((b'0' + d) as char);
}
} else {
for k in 0..=self.exponent as usize {
int.push((b'0' + self.digits.get(k).copied().unwrap_or(0)) as char);
}
for d in self.digits.iter().skip(self.exponent as usize + 1) {
frac.push((b'0' + d) as char);
}
}
(int, frac)
}
}
fn frac(v: f64) -> String {
let sign = if v < 0.0 { "-" } else { "" };
let a = v.abs();
let den = (1..=10_000u64).find(|d| ((a * *d as f64) - (a * *d as f64).round()).abs() < 1e-4 * *d as f64).unwrap_or(10_000);
let num = (a * den as f64).round() as u64;
let (whole, rest) = (num / den, num % den);
match (whole, rest) {
(w, 0) => format!("{sign}{w}"),
(0, r) => format!("{sign}\\frac{{{r}}}{{{den}}}"),
(w, r) => format!("{sign}{w}\\frac{{{r}}}{{{den}}}"),
}
}
impl NumberFormat {
pub fn print(&self, v: f64) -> String {
if v.is_nan() {
return "\\hbox{NaN}".into();
}
if v.is_infinite() {
return if v < 0.0 {
"-\\infty".into()
} else if self.showpos {
"+\\infty".into()
} else {
"\\infty".into()
};
}
let d = Decimal::parse(v);
match &self.style {
Style::Std { lower, upper } => {
let lower = lower.unwrap_or(-(self.precision as i32) / 2);
if !d.is_zero() && (d.exponent > *upper || d.exponent < lower) {
self.sci(d)
} else {
self.fixed(d)
}
}
Style::Fixed => self.fixed(d),
Style::Sci => self.sci(d),
Style::IntTrunc => {
let t = v.trunc();
self.fixed(Decimal::parse(if t == 0.0 { 0.0 } else { t }))
}
Style::IntDetect => {
let mut r = d.clone();
r.round_at(-6);
let (_, frac) = r.split();
if frac.is_empty() {
self.fixed(r)
} else {
self.sci(d)
}
}
Style::Relative(e) => {
let mut r = d.clone();
r.round_at(*e - self.precision as i32);
let f = NumberFormat { style: Style::Std { lower: None, upper: 4 }, precision: 9999, ..self.clone() };
if r.is_zero() {
return f.fixed(r);
}
f.print_decimal(r)
}
Style::FixedRelative => {
let mut r = d.clone();
r.round_at(d.exponent - self.precision as i32 + 1);
let f = NumberFormat { fixed_zerofill: false, precision: 9999, ..self.clone() };
f.fixed(r)
}
Style::Frac => frac(v),
}
}
fn print_decimal(&self, d: Decimal) -> String {
match &self.style {
Style::Std { lower, upper } => {
let lower = lower.unwrap_or(-2);
if d.exponent > *upper || d.exponent < lower {
self.sci(d)
} else {
self.fixed(d)
}
}
_ => self.fixed(d),
}
}
fn fixed(&self, mut d: Decimal) -> String {
d.round_at(-(self.precision as i32));
let (int, mut frac) = d.split();
if self.fixed_zerofill {
while frac.len() < self.precision {
frac.push('0');
}
}
let negative = d.negative && !d.is_zero();
let mut out = String::new();
if negative {
out.push('-');
} else if self.showpos {
out.push('+');
}
let skip = self.skip_zero && int == "0" && !frac.is_empty();
if !skip {
out.push_str(&self.group_thousands(&int, d.exponent));
}
if !frac.is_empty() {
out.push_str(&self.dec_sep);
out.push_str(&frac);
}
out
}
fn group_thousands(&self, int: &str, exponent: i32) -> String {
if self.thousands_sep.is_empty() || exponent < self.min_exponent_1000.max(0) || int.len() <= 3 {
return int.to_string();
}
let mut out = String::new();
let n = int.len();
for (i, c) in int.chars().enumerate() {
if i > 0 && (n - i).is_multiple_of(3) {
out.push_str(&self.thousands_sep);
}
out.push(c);
}
out
}
fn sci(&self, d: Decimal) -> String {
if d.is_zero() {
return self.fixed(d);
}
let precision = self.sci_precision.unwrap_or(self.precision);
let exponent = d.exponent;
let mut m = Decimal { negative: d.negative, digits: d.digits.clone(), exponent: 0 };
m.round_at(-(precision as i32));
let mut exponent = exponent;
if m.exponent > 0 {
m.exponent -= 1;
exponent += 1;
}
let mantissa_one = m.digits == [1] && m.exponent == 0;
let mut mf = self.clone();
mf.thousands_sep = String::new();
mf.fixed_zerofill = self.sci_zerofill;
mf.precision = precision;
let mantissa = mf.fixed(m.clone());
let (sep, exp) = match self.sci_style {
SciStyle::TenE => ("\\cdot ", format!("10^{{{exponent}}}")),
SciStyle::LowerE => ("", if exponent < 0 { format!("e{{-}}{}", -exponent) } else { format!("e{{+}}{exponent}") }),
SciStyle::UpperE => ("", if exponent < 0 { format!("E{{-}}{}", -exponent) } else { format!("E{{+}}{exponent}") }),
SciStyle::Subscript => ("", format!("_{{{exponent}}}")),
SciStyle::Superscript => ("", format!("^{{{exponent}}}")),
};
if mantissa_one && !self.unit_mantissa && self.sci_style == SciStyle::TenE {
return format!("{}{exp}", if m.negative { "-" } else { "" });
}
format!("{mantissa}{sep}{exp}")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn std(v: f64) -> String {
NumberFormat::default().print(v)
}
#[test]
fn std_style_like_pgf() {
assert_eq!(std(0.0), "0");
assert_eq!(std(2.0), "2");
assert_eq!(std(-6.0), "-6");
assert_eq!(std(0.5), "0.5");
assert_eq!(std(0.125), "0.13");
assert_eq!(std(1.0 / 3.0), "0.33");
assert_eq!(std(0.1 * 3.0), "0.3");
assert_eq!(std(1000.0), "1{,}000");
assert_eq!(std(12345.0), "12{,}345");
assert_eq!(std(123456.0), "1.23\\cdot 10^{5}");
assert_eq!(std(100000.0), "1\\cdot 10^{5}");
assert_eq!(std(0.05), "5\\cdot 10^{-2}");
assert_eq!(std(0.1), "0.1");
assert_eq!(std(-0.001), "-1\\cdot 10^{-3}");
assert_eq!(std(9.999), "10");
assert_eq!(std(-0.0001 + 0.0001), "0");
}
#[test]
fn fixed_sci_and_zerofill() {
let f = NumberFormat { style: Style::Fixed, precision: 3, ..Default::default() };
assert_eq!(f.print(1.23456), "1.235");
assert_eq!(f.print(123456.0), "123{,}456");
let f = NumberFormat { style: Style::Fixed, fixed_zerofill: true, ..Default::default() };
assert_eq!(f.print(2.5), "2.50");
assert_eq!(f.print(-0.001), "0.00");
let f = NumberFormat { style: Style::Sci, ..Default::default() };
assert_eq!(f.print(9.996e3), "1\\cdot 10^{4}");
assert_eq!(f.print(0.0), "0");
let f = NumberFormat { style: Style::IntTrunc, ..Default::default() };
assert_eq!(f.print(-3.7), "-3");
let f = NumberFormat { thousands_sep: String::new(), ..Default::default() };
assert_eq!(f.print(2000.0), "2000");
}
}