use std::fmt::{self, Write};
#[cfg(feature = "decimal")]
use rust_decimal::{Decimal, RoundingStrategy};
pub enum FormatStyle {
Euro,
PtBr,
Us,
}
pub trait FormattableNumber {
fn is_negative_num(&self) -> bool;
fn write_abs(&self, decimals: usize, buf: &mut String) -> fmt::Result;
}
macro_rules! impl_formattable_float {
($t:ty) => {
impl FormattableNumber for $t {
fn is_negative_num(&self) -> bool {
self.is_sign_negative()
}
fn write_abs(&self, decimals: usize, buf: &mut String) -> fmt::Result {
write!(buf, "{:.1$}", self.abs(), decimals)
}
}
};
}
impl_formattable_float!(f32);
impl_formattable_float!(f64);
#[cfg(feature = "decimal")]
impl FormattableNumber for Decimal {
fn is_negative_num(&self) -> bool {
self.is_sign_negative()
}
fn write_abs(&self, decimals: usize, buf: &mut String) -> fmt::Result {
let rounded = self
.abs()
.round_dp_with_strategy(decimals as u32, RoundingStrategy::MidpointNearestEven);
write!(buf, "{:.1$}", rounded, decimals)
}
}
impl<T: FormattableNumber> FormattableNumber for &T {
fn is_negative_num(&self) -> bool {
(*self).is_negative_num()
}
fn write_abs(&self, decimals: usize, buf: &mut String) -> fmt::Result {
(*self).write_abs(decimals, buf)
}
}
impl<T: FormattableNumber> FormattableNumber for Option<T> {
fn is_negative_num(&self) -> bool {
self.as_ref().is_some_and(|v| v.is_negative_num())
}
fn write_abs(&self, decimals: usize, buf: &mut String) -> fmt::Result {
match self {
Some(val) => val.write_abs(decimals, buf),
None => write!(buf, "{:.1$}", 0.0, decimals),
}
}
}
pub fn thousands_separator<T: FormattableNumber>(
value: T,
decimals: usize,
style: FormatStyle,
) -> String {
let temp_capacity = 20 + decimals;
let mut abs_temp = String::with_capacity(temp_capacity);
let _ = value.write_abs(decimals, &mut abs_temp);
let (thousands_sep, decimal_sep) = match style {
FormatStyle::Euro | FormatStyle::PtBr => ('.', ","),
FormatStyle::Us => (',', "."),
};
let (integer_part, fraction_part) = match abs_temp.split_once('.') {
Some((i, f)) if decimals > 0 => (i, Some(f)),
_ => (abs_temp.as_str(), None),
};
let is_neg = value.is_negative_num();
let num_seps = integer_part.len().saturating_sub(1) / 3;
let final_capacity = abs_temp.len() + (num_seps * thousands_sep.len_utf8()) + (is_neg as usize);
let mut result = String::with_capacity(final_capacity);
if is_neg {
result.push('-');
}
add_sep(integer_part, thousands_sep, &mut result);
if let Some(f) = fraction_part {
result.push_str(decimal_sep);
result.push_str(f);
}
result
}
pub fn add_sep(integer: &str, separator: char, buffer: &mut String) {
let len = integer.len();
if len == 0 {
return;
}
for (i, &byte) in integer.as_bytes().iter().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
buffer.push(separator);
}
buffer.push(byte as char);
}
}
#[cfg(test)]
mod separator_tests {
use super::*;
#[test]
fn thousands_separator_test() {
let val_f32: &f32 = &-5000.0;
let result = thousands_separator(val_f32, 2, FormatStyle::PtBr);
println!("f32: {val_f32}");
println!("result: {result}\n");
assert_eq!(result, "-5.000,00");
let val_f64: f64 = -1234567.8949;
let result = thousands_separator(val_f64, 2, FormatStyle::PtBr);
println!("f64: {val_f64}");
println!("result: {result}\n");
assert_eq!(result, "-1.234.567,89");
let val_f64_round: f64 = -1234567.8950;
let result = thousands_separator(val_f64_round, 2, FormatStyle::PtBr);
println!("f64: {val_f64_round}");
println!("result: {result}\n");
assert_eq!(result, "-1.234.567,90");
let val_us = 1234567.8912;
let result = thousands_separator(val_us, 2, FormatStyle::Us);
println!("f64: {val_us}");
println!("us result: {result}\n");
assert_eq!(result, "1,234,567.89");
}
#[test]
fn test_options() {
let val_some: Option<f64> = Some(1234.567);
let result = thousands_separator(val_some, 2, FormatStyle::PtBr);
println!("val_some: {val_some:?}");
println!("result: {result}\n");
assert_eq!(result, "1.234,57");
let val_none: Option<f64> = None;
let result = thousands_separator(val_none, 2, FormatStyle::Us);
println!("val_none: {val_none:?}");
println!("result: {result}\n");
assert_eq!(result, "0.00");
let result_no_dec = thousands_separator(val_none, 0, FormatStyle::Euro);
assert_eq!(result_no_dec, "0");
}
#[cfg(feature = "decimal")]
#[test]
fn test_decimal() {
let val_decimal = Decimal::new(12345678850, 4); let result = thousands_separator(val_decimal, 2, FormatStyle::PtBr);
println!("decimal: {val_decimal}");
println!("result: {result}\n");
assert_eq!(result, "1.234.567,88");
let val_decimal = Decimal::new(12345678950, 4); let result = thousands_separator(val_decimal, 2, FormatStyle::PtBr);
println!("decimal: {val_decimal}");
println!("result: {result}\n");
assert_eq!(result, "1.234.567,90");
let val_decimal: Decimal = Decimal::new(12345678951, 4); let result = thousands_separator(val_decimal, 3, FormatStyle::PtBr);
println!("decimal: {val_decimal}");
println!("result: {result}\n");
assert_eq!(result, "1.234.567,895");
let opt_decimal: Option<Decimal> = Some(Decimal::new(50000000, 0));
assert_eq!(
thousands_separator(opt_decimal, 4, FormatStyle::PtBr),
"50.000.000,0000"
);
let opt_decimal: Option<Decimal> = Some(Decimal::new(50000000, 0));
assert_eq!(
thousands_separator(opt_decimal, 3, FormatStyle::Us),
"50,000,000.000"
);
}
}