#![warn(missing_docs)]
use std::{cmp::*, error, fmt, hash::*};
use Precision::*;
mod numeric;
mod parse;
#[cfg(test)]
mod tests;
pub use numeric::Numeric;
pub use parse::ParseError;
pub type Result = std::result::Result<Formatter, Error>;
const SN_BIG_CUTOFF: f64 = 1_000_000_000_000f64;
const SN_SML_CUTOFF: f64 = 0.001;
const SN_PREC: Precision = Significance(7);
const PREFIX_LIM: usize = 12;
const UNITS_LIM: usize = 12;
const SUFFIX_LIM: usize = 12;
const FLOATBUF_LEN: usize = 22;
const BUF_LEN: usize = PREFIX_LIM + FLOATBUF_LEN + 3 + UNITS_LIM + SUFFIX_LIM;
#[derive(Debug, Clone)]
pub struct Formatter {
strbuf: Vec<u8>,
thou_sep: Option<u8>,
comma: bool,
start: usize,
precision: Precision,
scales: Scales,
suffix: [u8; SUFFIX_LIM],
suffix_len: usize,
convert: fn(f64) -> f64,
}
impl Formatter {
pub fn new() -> Self {
Self {
strbuf: vec![0; BUF_LEN],
thou_sep: None,
start: 0,
precision: Precision::Unspecified,
scales: Scales::none(),
suffix: [0; SUFFIX_LIM],
suffix_len: 0,
convert: |x| x,
comma: false,
}
}
pub fn currency(prefix: &str) -> Result {
Self::new()
.separator(',')
.unwrap()
.precision(Decimals(2))
.prefix(prefix)
}
pub fn percentage() -> Self {
Self::new().convert(|x| x * 100.0).suffix("%").unwrap()
}
pub fn convert(mut self, f: fn(f64) -> f64) -> Self {
self.convert = f;
self
}
pub fn precision(mut self, precision: Precision) -> Self {
self.precision = precision;
self
}
pub fn scales(mut self, scales: Scales) -> Self {
self.scales = scales;
self
}
pub fn build_scales(mut self, base: u16, units: Vec<&'static str>) -> Result {
let scales = Scales::new(base, units)?;
self.scales = scales;
Ok(self)
}
pub fn separator<S: Into<Option<char>>>(mut self, sep: S) -> Result {
if let Some(sep) = sep.into() {
if sep.len_utf8() != 1 {
Err(Error::InvalidSeparator(sep))
} else {
if sep == '.' {
self.comma = true;
}
let mut buf = [0];
sep.encode_utf8(&mut buf);
self.thou_sep = Some(buf[0]);
Ok(self)
}
} else {
self.thou_sep = None;
Ok(self)
}
}
pub fn comma(mut self, comma: bool) -> Self {
self.comma = comma;
if comma && self.thou_sep == Some(b',') {
self.thou_sep = Some(b'.');
}
self
}
pub fn prefix(mut self, prefix: &str) -> Result {
if prefix.len() > PREFIX_LIM {
Err(Error::InvalidPrefix(prefix.to_string()))
} else {
let n = prefix.len();
self.strbuf[..n].copy_from_slice(prefix.as_bytes());
self.start = n;
Ok(self)
}
}
pub fn suffix(mut self, suffix: &str) -> Result {
if suffix.len() > SUFFIX_LIM {
Err(Error::InvalidSuffix(suffix.to_string()))
} else {
let n = suffix.len();
self.suffix[..n].copy_from_slice(suffix.as_bytes());
self.suffix_len = n;
Ok(self)
}
}
#[deprecated = "consider using Formatter::fmt2 instead"]
pub fn fmt(&mut self, num: f64) -> &str {
self.fmt2(num)
}
pub fn fmt2<N: Numeric>(&mut self, num: N) -> &str {
let mut buf = std::mem::take(&mut self.strbuf);
let bytes = self.fmt_into_buf(&mut buf, num);
self.strbuf = buf;
std::str::from_utf8(&self.strbuf[..bytes]).expect("will be valid string")
}
pub fn fmt_into<N: Numeric>(&self, buf: &mut String, num: N) {
let start = buf.len();
buf.extend(std::iter::repeat_n('\0', self.strbuf.len()));
let bytes = unsafe { buf.as_bytes_mut() };
bytes[start..start + self.start].copy_from_slice(&self.strbuf[..self.start]);
let written = self.fmt_into_buf(&mut bytes[start..], num);
let end = start + written;
buf.truncate(end);
debug_assert!(std::str::from_utf8(buf.as_bytes()).is_ok());
}
pub fn fmt_string<N: Numeric>(&self, num: N) -> String {
let mut buf = String::new();
self.fmt_into(&mut buf, num);
buf
}
fn fmt_into_buf<N: Numeric>(&self, strbuf: &mut [u8], num: N) -> usize {
debug_assert_eq!(
strbuf.len(),
BUF_LEN,
"the buffer is expected to be {BUF_LEN} wide"
);
if num.is_nan() {
strbuf[..3].copy_from_slice(b"NaN");
3
} else if num.is_infinite() && num.is_negative() {
strbuf[..4].copy_from_slice(b"-\xE2\x88\x9E"); 4
} else if num.is_infinite() {
strbuf[..3].copy_from_slice(b"\xE2\x88\x9E");
3
} else if num.is_zero() {
strbuf[..1].copy_from_slice(b"0");
1
} else {
let num = (self.convert)(num.to_f64());
let (scaled, unit) = self.scales.scale(num);
let abs = scaled.abs();
let sn_sml_cutoff = match self.precision {
Decimals(d) | Significance(d) if d <= 3 => 10f64.powi(d as i32).recip(),
_ => SN_SML_CUTOFF,
};
if abs >= SN_BIG_CUTOFF || abs < sn_sml_cutoff {
let (num, exponent) = reduce_to_sn(num);
let precision = match self.precision {
Unspecified => SN_PREC,
x => x,
};
let cursor = self.start + self.write_num(strbuf, num, precision);
strbuf[cursor] = b'e'; let cursor = 1 + cursor;
let written = {
let mut buf = itoa::Buffer::new();
let s = buf.format(exponent);
let end = cursor + s.len();
strbuf[cursor..end].copy_from_slice(s.as_bytes());
s.len()
};
let cursor = cursor + written;
self.apply_suffix(strbuf, cursor)
} else {
let mut cursor = self.start + self.write_num(strbuf, scaled, self.precision);
if !unit.is_empty() {
let s = cursor;
cursor += unit.len();
strbuf[s..cursor].copy_from_slice(unit.as_bytes());
}
self.apply_suffix(strbuf, cursor)
}
}
}
fn write_num(&self, strbuf: &mut [u8], num: f64, precision: Precision) -> usize {
let mut tmp = dtoa::Buffer::new();
let s = tmp.format(num);
let tmp = s.as_bytes();
let n = tmp.len();
let mut digits = 0;
let mut written = 0;
let mut in_frac = false;
let mut thou = 2 - (num.abs().log10().trunc() as u8) % 3;
let mut idx = self.start;
for i in 0..n {
let byte = tmp[i]; strbuf[idx] = byte; idx += 1;
written += 1;
if byte.is_ascii_digit() {
digits += 1;
thou += 1;
}
if i + 1 < n && tmp[i + 1] == b'.' {
in_frac = true;
if let Decimals(_) = precision {
digits = 0
}
} else if in_frac && byte == b'.' && self.comma {
strbuf[idx - 1] = b',';
} else if !in_frac && thou == 3 {
if let Some(sep) = self.thou_sep {
thou = 0;
strbuf[idx] = sep;
idx += 1;
written += 1;
}
}
match precision {
Significance(d) | Decimals(d) if in_frac => {
if digits >= d {
break;
}
}
_ => (),
}
}
written
}
fn apply_suffix(&self, strbuf: &mut [u8], mut pos: usize) -> usize {
if !self.suffix.is_empty() {
let s = pos;
pos = s + self.suffix_len;
strbuf[s..pos].copy_from_slice(&self.suffix[..self.suffix_len]);
}
pos
}
}
impl Default for Formatter {
fn default() -> Self {
Self::new()
.separator(',')
.unwrap()
.scales(Scales::short())
.precision(Decimals(3))
}
}
impl std::str::FromStr for Formatter {
type Err = parse::ParseError;
fn from_str(s: &str) -> std::result::Result<Self, ParseError> {
parse::parse_formatter(s)
}
}
impl PartialEq for Formatter {
#[allow(clippy::suspicious_operation_groupings)]
fn eq(&self, other: &Self) -> bool {
std::ptr::fn_addr_eq(self.convert, other.convert)
&& self.precision == other.precision
&& self.thou_sep == other.thou_sep
&& self.suffix[..self.suffix_len] == other.suffix[..other.suffix_len]
&& self.strbuf[..self.start] == other.strbuf[..other.start]
&& self.scales == other.scales
}
}
impl Eq for Formatter {}
impl Hash for Formatter {
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.strbuf[..self.start].hash(hasher);
self.thou_sep.hash(hasher);
self.precision.hash(hasher);
self.scales.hash(hasher);
self.suffix[..self.suffix_len].hash(hasher);
self.convert.hash(hasher);
}
}
fn reduce_to_sn(n: f64) -> (f64, i32) {
if n == 0.0 || n == -0.0 {
(0.0, 0)
} else {
let abs = n.abs();
let mut e = abs.log10().trunc() as i32;
if abs < 1.0 {
e -= 1;
}
let n = n * 10_f64.powi(-e);
(n, e)
}
}
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidPrefix(String),
InvalidSeparator(char),
InvalidSuffix(String),
InvalidUnit(&'static str),
ZeroBase,
}
impl error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*;
match self {
InvalidPrefix(prefix) => write!(
f,
"Invalid prefix `{prefix}`. Prefix is longer than the supported {PREFIX_LIM} bytes"
),
InvalidSeparator(sep) => write!(
f,
"Invalid separator `{sep}`. Separator can only be one byte long"
),
InvalidSuffix(suffix) => write!(
f,
"Invalid suffix `{suffix}`. Suffix is longer than the supported {SUFFIX_LIM} bytes"
),
InvalidUnit(unit) => write!(
f,
"Invalid unit `{unit}`. Unit is longer than the supported {UNITS_LIM} bytes"
),
ZeroBase => write!(f, "Invalid scale base, base must be greater than zero"),
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)]
#[allow(missing_docs)]
pub enum Precision {
Significance(u8),
Decimals(u8),
Unspecified,
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct Scales {
base: u16,
units: Vec<&'static str>,
}
impl Scales {
pub fn new(base: u16, units: Vec<&'static str>) -> std::result::Result<Self, Error> {
if base == 0 {
return Err(Error::ZeroBase);
}
for unit in &units {
if unit.len() > UNITS_LIM {
return Err(Error::InvalidUnit(unit));
}
}
Ok(Self { base, units })
}
pub fn none() -> Self {
Self {
base: u16::MAX,
units: Vec::new(),
}
}
pub fn short() -> Self {
Scales {
base: 1000,
units: vec!["", " K", " M", " B", " T", " P", " E", " Z", " Y"],
}
}
pub fn metric() -> Self {
Scales {
base: 1000,
units: vec![" ", " k", " M", " G", " T", " P", " E", " Z", " Y"],
}
}
pub fn binary() -> Self {
Scales {
base: 1024,
units: vec![" ", " ki", " Mi", " Gi", " Ti", " Pi", " Ei", " Zi", " Yi"],
}
}
pub fn base(&self) -> u16 {
self.base
}
pub fn units(&self) -> &[&'static str] {
self.units.as_slice()
}
pub fn into_inner(self) -> (u16, Vec<&'static str>) {
(self.base, self.units)
}
pub fn scale(&self, mut num: f64) -> (f64, &'static str) {
let base = self.base as f64;
let mut u = "";
let mut n2 = num;
for unit in &self.units {
num = n2;
u = unit;
if num.abs() >= base {
n2 = num / base;
} else {
break;
}
}
(num, u)
}
}