use crate::currency::FormattableCurrency;
use crate::{Money, Round};
use std::cmp::Ordering;
pub struct Formatter;
impl Formatter {
pub fn money<'a, T: FormattableCurrency>(money: &Money<'a, T>, params: Params<'_>) -> String {
let mut decimal = *money.amount();
if let Some(x) = params.rounding {
decimal = *money.round(x, Round::HalfEven).amount();
decimal.rescale(x);
}
let amount = Formatter::amount(&format!("{}", decimal), ¶ms);
let mut result = String::new();
for position in params.positions.iter() {
match position {
Position::Space => result.push(' '),
Position::Amount => result.push_str(&amount),
Position::Code => result.push_str(params.code.unwrap_or("")),
Position::Symbol => result.push_str(params.symbol.unwrap_or("")),
Position::Sign => result.push_str(if money.is_negative() { "-" } else { "" }),
}
}
result
}
fn amount(raw_amount: &str, params: &Params<'_>) -> String {
let amount_split: Vec<&str> = raw_amount.split('.').collect();
let mut amount_digits = amount_split[0].to_string();
amount_digits.retain(|c| c != '-');
amount_digits = Formatter::digits(
&amount_digits,
params.digit_separator,
params.separator_pattern,
);
let mut result = amount_digits;
match amount_split.len().cmp(&2) {
Ordering::Equal => {
result.push(params.exponent_separator);
result += amount_split[1];
}
Ordering::Less => {
}
Ordering::Greater => {
unreachable!(
"Decimal formatted string should never contain more than 1 exponent separator"
)
}
}
result
}
fn digits(raw_digits: &str, separator: char, pattern: &[usize]) -> String {
let mut digits = raw_digits.to_string();
let mut current_position: usize = 0;
for &position in pattern.iter() {
current_position += position;
if digits.len() > current_position {
digits.insert(digits.len() - current_position, separator);
current_position += 1;
}
}
digits
}
}
#[derive(Debug, Clone)]
pub enum Position {
Space,
Amount,
Code,
Symbol,
Sign,
}
#[derive(Debug, Clone)]
pub struct Params<'a> {
pub digit_separator: char,
pub exponent_separator: char,
pub separator_pattern: &'a [usize],
pub positions: &'a [Position],
pub rounding: Option<u32>,
pub symbol: Option<&'static str>,
pub code: Option<&'static str>,
}
const DEFAULT_SEPARATOR_PATTERN: &[usize] = &[3, 3, 3];
const DEFAULT_POSITIONS: &[Position] = &[Position::Sign, Position::Symbol, Position::Amount];
impl Default for Params<'_> {
fn default() -> Self {
Params {
digit_separator: ',',
exponent_separator: '.',
separator_pattern: DEFAULT_SEPARATOR_PATTERN,
positions: DEFAULT_POSITIONS,
rounding: None,
symbol: None,
code: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::define_currency_set;
define_currency_set!(
test {
USD: {
code: "USD",
exponent: 2,
locale: EnUs,
minor_units: 100,
name: "USD",
symbol: "$",
symbol_first: true,
}
}
);
#[test]
fn format_position() {
let _usd = test::find("USD");
let money = Money::from_major(-1000, test::USD);
let params = Params {
symbol: Some("$"),
code: Some("USD"),
positions: &[
Position::Sign,
Position::Space,
Position::Symbol,
Position::Amount,
Position::Space,
Position::Code,
],
..Default::default()
};
assert_eq!("- $1,000 USD", Formatter::money(&money, params));
let params = Params {
symbol: Some("$"),
code: Some("USD"),
positions: &[
Position::Code,
Position::Space,
Position::Amount,
Position::Symbol,
Position::Space,
Position::Sign,
],
..Default::default()
};
assert_eq!("USD 1,000$ -", Formatter::money(&money, params));
let params = Params {
positions: &[Position::Amount],
..Default::default()
};
assert_eq!("1,000", Formatter::money(&money, params));
let params = Params {
symbol: Some("$"),
positions: &[Position::Symbol],
..Default::default()
};
assert_eq!("$", Formatter::money(&money, params));
let params = Params {
positions: &[Position::Amount, Position::Symbol],
..Default::default()
};
assert_eq!("1,000", Formatter::money(&money, params));
let params = Params {
symbol: Some("$"),
positions: &[Position::Symbol, Position::Sign, Position::Amount],
..Default::default()
};
assert_eq!("$-1,000", Formatter::money(&money, params));
}
#[test]
fn format_digit_separators_with_custom_separators() {
let params = Params {
digit_separator: '/',
..Default::default()
};
let money = Money::from_major(1_000_000, test::USD);
assert_eq!("1/000/000", Formatter::money(&money, params.clone()));
let money = Money::from_major(1_000, test::USD);
assert_eq!("1/000", Formatter::money(&money, params.clone()));
let money = Money::from_major(0, test::USD);
assert_eq!("0", Formatter::money(&money, params));
let params = Params {
rounding: Some(2),
exponent_separator: ',',
digit_separator: '.',
..Default::default()
};
let money = Money::from_minor(123456, test::USD);
assert_eq!("1.234,56", Formatter::money(&money, params));
}
#[test]
fn format_digit_separators_with_custom_sequences() {
let params = Params {
separator_pattern: &[3, 2, 2],
..Default::default()
};
let money = Money::from_major(10_000_000, test::USD);
assert_eq!("1,00,00,000", Formatter::money(&money, params.clone()));
let money = Money::from_major(100_000, test::USD);
assert_eq!("1,00,000", Formatter::money(&money, params.clone()));
let money = Money::from_major(1_000, test::USD);
assert_eq!("1,000", Formatter::money(&money, params));
}
#[test]
fn format_zero_amount() {
let params = Params {
symbol: Some("$"),
positions: &[Position::Sign, Position::Symbol, Position::Amount],
..Default::default()
};
let money = Money::from_major(0, test::USD);
assert_eq!("$0", Formatter::money(&money, params));
}
#[test]
fn format_rounding() {
let money = Money::from_minor(1000, test::USD).div(3).unwrap();
let params = Params {
rounding: Some(0),
..Default::default()
};
assert_eq!("3", Formatter::money(&money, params));
let params = Params {
rounding: Some(2),
..Default::default()
};
assert_eq!("3.33", Formatter::money(&money, params));
let params = Params {
..Default::default()
};
assert_eq!(
"3.3333333333333333333333333333",
Formatter::money(&money, params)
);
}
}
#[cfg(all(test, feature = "iso"))]
mod golden_tests {
use crate::Money;
use crate::iso;
#[test]
fn usd_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::USD)), "$0.00");
assert_eq!(format!("{}", Money::from_minor(1, iso::USD)), "$0.01");
assert_eq!(format!("{}", Money::from_minor(100, iso::USD)), "$1.00");
assert_eq!(
format!("{}", Money::from_minor(123456, iso::USD)),
"$1,234.56"
);
assert_eq!(
format!("{}", Money::from_minor(123456789, iso::USD)),
"$1,234,567.89"
);
assert_eq!(format!("{}", Money::from_minor(-100, iso::USD)), "-$1.00");
assert_eq!(
format!("{}", Money::from_minor(-123456, iso::USD)),
"-$1,234.56"
);
}
#[test]
fn eur_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::EUR)), "€0,00");
assert_eq!(
format!("{}", Money::from_minor(123456, iso::EUR)),
"€1.234,56"
);
assert_eq!(
format!("{}", Money::from_minor(-123456, iso::EUR)),
"-€1.234,56"
);
}
#[test]
fn gbp_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::GBP)), "£0.00");
assert_eq!(
format!("{}", Money::from_minor(123456, iso::GBP)),
"£1,234.56"
);
}
#[test]
fn jpy_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::JPY)), "¥0");
assert_eq!(format!("{}", Money::from_minor(1, iso::JPY)), "¥1");
assert_eq!(format!("{}", Money::from_minor(1234, iso::JPY)), "¥1,234");
assert_eq!(
format!("{}", Money::from_minor(1234567, iso::JPY)),
"¥1,234,567"
);
}
#[test]
fn inr_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::INR)), "₹0.00");
assert_eq!(format!("{}", Money::from_minor(100, iso::INR)), "₹1.00");
assert_eq!(
format!("{}", Money::from_minor(10000000, iso::INR)),
"₹1,00,000.00"
);
assert_eq!(
format!("{}", Money::from_minor(1000000000, iso::INR)),
"₹1,00,00,000.00"
);
}
#[test]
fn bhd_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::BHD)), "د.ب0.000");
assert_eq!(format!("{}", Money::from_minor(1, iso::BHD)), "د.ب0.001");
assert_eq!(format!("{}", Money::from_minor(1000, iso::BHD)), "د.ب1.000");
assert_eq!(
format!("{}", Money::from_minor(1234567, iso::BHD)),
"د.ب1,234.567"
);
}
#[test]
fn byn_format_golden() {
assert_eq!(format!("{}", Money::from_minor(0, iso::BYN)), "0,00Br");
assert_eq!(
format!("{}", Money::from_minor(123456, iso::BYN)),
"1 234,56Br"
);
assert_eq!(
format!("{}", Money::from_minor(123456789, iso::BYN)),
"1 234 567,89Br"
);
}
}