pub trait PriceFormatter: Send + Sync {
fn format(&self, price: f64) -> String;
fn clone_box(&self) -> Box<dyn PriceFormatter>;
}
#[derive(Debug, Clone)]
pub struct DefaultPriceFormatter {
pub precision: usize,
pub min_precision: usize,
}
impl Default for DefaultPriceFormatter {
fn default() -> Self {
Self {
precision: 2,
min_precision: 0,
}
}
}
impl DefaultPriceFormatter {
pub fn with_precision(precision: usize) -> Self {
Self {
precision,
min_precision: precision,
}
}
pub fn auto() -> Self {
Self {
precision: 8,
min_precision: 0,
}
}
}
impl PriceFormatter for DefaultPriceFormatter {
fn format(&self, price: f64) -> String {
if self.min_precision > 0 {
format!("{:.prec$}", price, prec = self.precision)
} else {
let formatted = format!("{:.prec$}", price, prec = self.precision);
formatted
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct PercentageFormatter {
pub precision: usize,
pub base_val: f64,
}
impl Default for PercentageFormatter {
fn default() -> Self {
Self {
precision: 2,
base_val: 100.0,
}
}
}
impl PriceFormatter for PercentageFormatter {
fn format(&self, price: f64) -> String {
if self.base_val.abs() < f64::EPSILON {
return format!("{:+.prec$}%", 0.0, prec = self.precision);
}
let percentage = ((price / self.base_val) * 100.0) - 100.0;
format!("{:+.prec$}%", percentage, prec = self.precision)
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct CurrencyFormatter {
pub symbol: String,
pub precision: usize,
pub symbol_before: bool,
pub add_space: bool,
pub thousands_sep: Option<char>,
}
impl CurrencyFormatter {
pub fn usd() -> Self {
Self {
symbol: "$".to_string(),
precision: 2,
symbol_before: true,
add_space: false,
thousands_sep: Some(','),
}
}
pub fn eur() -> Self {
Self {
symbol: "€".to_string(),
precision: 2,
symbol_before: false,
add_space: true,
thousands_sep: Some('.'),
}
}
pub fn btc() -> Self {
Self {
symbol: "₿".to_string(),
precision: 8,
symbol_before: false,
add_space: true,
thousands_sep: Some(','),
}
}
pub fn new(symbol: impl Into<String>, precision: usize) -> Self {
Self {
symbol: symbol.into(),
precision,
symbol_before: true,
add_space: false,
thousands_sep: Some(','),
}
}
}
impl PriceFormatter for CurrencyFormatter {
fn format(&self, price: f64) -> String {
let val_str = if let Some(sep) = self.thousands_sep {
self.format_with_separator(price, sep)
} else {
format!("{:.prec$}", price, prec = self.precision)
};
let space = if self.add_space { " " } else { "" };
if self.symbol_before {
format!("{}{}{}", self.symbol, space, val_str)
} else {
format!("{}{}{}", val_str, space, self.symbol)
}
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(self.clone())
}
}
impl CurrencyFormatter {
fn format_with_separator(&self, price: f64, separator: char) -> String {
let formatted = format!("{:.prec$}", price, prec = self.precision);
let parts: Vec<&str> = formatted.split('.').collect();
let integer = parts[0];
let decimal = if parts.len() > 1 { parts[1] } else { "" };
let integer_with_sep: String = integer
.chars()
.rev()
.enumerate()
.fold(String::new(), |mut acc, (i, c)| {
if i > 0 && i % 3 == 0 {
acc.push(separator);
}
acc.push(c);
acc
})
.chars()
.rev()
.collect();
if decimal.is_empty() {
integer_with_sep
} else {
format!("{integer_with_sep}.{decimal}")
}
}
}
#[derive(Debug, Clone)]
pub struct VolumeFormatter {
pub precision: usize,
}
impl Default for VolumeFormatter {
fn default() -> Self {
Self { precision: 2 }
}
}
impl PriceFormatter for VolumeFormatter {
fn format(&self, value: f64) -> String {
let abs_val = value.abs();
if abs_val >= 1_000_000_000.0 {
format!("{:.prec$}B", value / 1_000_000_000.0, prec = self.precision)
} else if abs_val >= 1_000_000.0 {
format!("{:.prec$}M", value / 1_000_000.0, prec = self.precision)
} else if abs_val >= 1_000.0 {
format!("{:.prec$}K", value / 1_000.0, prec = self.precision)
} else {
format!("{:.prec$}", value, prec = self.precision)
}
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(self.clone())
}
}
#[derive(Debug, Clone)]
pub struct ScientificFormatter {
pub precision: usize,
}
impl Default for ScientificFormatter {
fn default() -> Self {
Self { precision: 3 }
}
}
impl PriceFormatter for ScientificFormatter {
fn format(&self, price: f64) -> String {
format!("{:.prec$e}", price, prec = self.precision)
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(self.clone())
}
}
pub struct CustomPriceFormatter {
formatter: Box<dyn Fn(f64) -> String + Send + Sync>,
}
impl CustomPriceFormatter {
pub fn new<F>(formatter: F) -> Self
where
F: Fn(f64) -> String + Send + Sync + 'static,
{
Self {
formatter: Box::new(formatter),
}
}
}
impl PriceFormatter for CustomPriceFormatter {
fn format(&self, price: f64) -> String {
(self.formatter)(price)
}
fn clone_box(&self) -> Box<dyn PriceFormatter> {
Box::new(DefaultPriceFormatter::default())
}
}
pub struct PriceFormatterBuilder {
precision: usize,
currency: Option<String>,
percentage: bool,
volume: bool,
}
impl PriceFormatterBuilder {
pub fn new() -> Self {
Self {
precision: 2,
currency: None,
percentage: false,
volume: false,
}
}
pub fn with_precision(mut self, precision: usize) -> Self {
self.precision = precision;
self
}
pub fn with_currency(mut self, symbol: impl Into<String>) -> Self {
self.currency = Some(symbol.into());
self
}
pub fn as_percentage(mut self) -> Self {
self.percentage = true;
self
}
pub fn as_volume(mut self) -> Self {
self.volume = true;
self
}
pub fn build(self) -> Box<dyn PriceFormatter> {
if self.percentage {
Box::new(PercentageFormatter {
precision: self.precision,
base_val: 100.0,
})
} else if self.volume {
Box::new(VolumeFormatter {
precision: self.precision,
})
} else if let Some(symbol) = self.currency {
Box::new(CurrencyFormatter::new(symbol, self.precision))
} else {
Box::new(DefaultPriceFormatter::with_precision(self.precision))
}
}
}
impl Default for PriceFormatterBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_formatter() {
let formatter = DefaultPriceFormatter::default();
assert_eq!(formatter.format(123.456), "123.46");
assert_eq!(formatter.format(123.0), "123");
}
#[test]
fn test_fixed_precision() {
let formatter = DefaultPriceFormatter::with_precision(4);
assert_eq!(formatter.format(123.456), "123.4560");
}
#[test]
fn test_percentage_formatter() {
let formatter = PercentageFormatter::default();
assert_eq!(formatter.format(105.0), "+5.00%");
assert_eq!(formatter.format(95.0), "-5.00%");
}
#[test]
fn test_percentage_formatter_zero_base_is_neutral() {
let formatter = PercentageFormatter {
precision: 2,
base_val: 0.0,
};
let out = formatter.format(123.0);
assert_eq!(out, "+0.00%");
assert!(!out.contains("inf") && !out.contains("NaN"));
}
#[test]
fn test_currency_formatter() {
let usd = CurrencyFormatter::usd();
assert_eq!(usd.format(1234.56), "$1,234.56");
let eur = CurrencyFormatter::eur();
assert!(eur.format(1234.56).contains("€"));
let btc = CurrencyFormatter::btc();
assert!(btc.format(0.12345678).contains("₿"));
}
#[test]
fn test_volume_formatter() {
let formatter = VolumeFormatter::default();
assert_eq!(formatter.format(1_500_000_000.0), "1.50B");
assert_eq!(formatter.format(2_500_000.0), "2.50M");
assert_eq!(formatter.format(3_500.0), "3.50K");
assert_eq!(formatter.format(500.0), "500.00");
}
#[test]
fn test_scientific_formatter() {
let formatter = ScientificFormatter::default();
let result = formatter.format(0.000123);
assert!(result.contains("e"));
}
#[test]
fn test_custom_formatter() {
let formatter = CustomPriceFormatter::new(|price| format!("Price: {price:.2}"));
assert_eq!(formatter.format(123.456), "Price: 123.46");
}
#[test]
fn test_formatter_builder() {
let formatter = PriceFormatterBuilder::new().with_precision(4).build();
assert_eq!(formatter.format(123.456789), "123.4568");
let currency_formatter = PriceFormatterBuilder::new()
.with_currency("$")
.with_precision(2)
.build();
assert!(currency_formatter.format(100.0).contains("$"));
}
}