use icu::{
experimental::dimension::currency::{formatter::CurrencyFormatter, CurrencyCode},
locale::Locale,
};
use rust_decimal::RoundingStrategy;
use std::str::FromStr;
use tinystr::TinyAsciiStr;
use crate::{Currency, Money};
pub use icu::experimental::dimension::currency::options::CurrencyFormatterOptions;
#[derive(Debug, Clone)]
pub struct FormattingOptions {
pub decimal_places: u32,
pub rounding_strategy: RoundingStrategy,
pub currency_formatter_options: CurrencyFormatterOptions,
}
impl<C> Money<C> {
fn format_helper(
&self,
locale: &Locale,
currency_code_str: &'static str,
options: FormattingOptions,
) -> String {
let currency_code = CurrencyCode(
TinyAsciiStr::from_str(currency_code_str).expect("unsupported currency code"),
);
let formatter =
CurrencyFormatter::try_new(locale.into(), CurrencyFormatterOptions::default()).unwrap();
let mut rounded_amount = self
.amount
.round_dp_with_strategy(options.decimal_places, options.rounding_strategy);
rounded_amount.rescale(options.decimal_places);
let amount_string = rounded_amount.to_string();
let amount = icu::decimal::input::Decimal::try_from_str(&amount_string).unwrap();
let formatted = formatter.format_fixed_decimal(&amount, currency_code);
formatted.to_string()
}
}
impl<C> Money<C>
where
C: Currency + Copy,
{
pub fn format(&self, locale: &Locale) -> String {
self.format_helper(
locale,
self.currency.code(),
FormattingOptions {
decimal_places: self.currency.minor_units(),
rounding_strategy: RoundingStrategy::MidpointNearestEven,
currency_formatter_options: CurrencyFormatterOptions::default(),
},
)
}
pub fn format_with_options(&self, locale: &Locale, options: FormattingOptions) -> String {
self.format_helper(locale, self.currency.code(), options)
}
}
impl Money<&dyn Currency> {
pub fn format(&self, locale: &Locale) -> String {
self.format_helper(
locale,
self.currency.code(),
FormattingOptions {
decimal_places: self.currency.minor_units(),
rounding_strategy: RoundingStrategy::MidpointNearestEven,
currency_formatter_options: CurrencyFormatterOptions::default(),
},
)
}
pub fn format_with_options(&self, locale: &Locale, options: FormattingOptions) -> String {
self.format_helper(locale, self.currency.code(), options)
}
}
#[cfg(test)]
mod tests {
use crate::formatting::*;
use crate::iso_currencies::{EUR, JPY, PLN, USD};
use crate::*;
use icu::locale::locale;
#[test]
fn locale_aware_formatting() {
let m = Money::new(Decimal::new(123456789, 2), EUR);
assert_eq!(m.format(&locale!("en-US")), "€1,234,567.89");
assert_eq!(m.format(&locale!("ir-IR")), "€\u{a0}1,234,567.89");
assert_eq!(m.format(&locale!("tr-TR")), "€1.234.567,89");
assert_eq!(
m.format(&locale!("fr-FR")),
"1\u{202f}234\u{202f}567,89\u{a0}€"
);
assert_eq!(m.format(&locale!("pl-PL")), "1\u{a0}234\u{a0}567,89\u{a0}€");
}
#[test]
fn format_pln() {
let m = Money::new(Decimal::new(123456789, 2), PLN);
assert_eq!(
m.format(&locale!("pl-PL")),
"1\u{a0}234\u{a0}567,89\u{a0}zł"
);
assert_eq!(m.format(&locale!("en-US")), "PLN\u{a0}1,234,567.89");
}
#[test]
fn format_dyn_currency() {
let c: &dyn Currency = &USD;
let m = Money::new(Decimal::new(123456789, 2), c);
assert_eq!(m.format(&locale!("en-US")), "$1,234,567.89");
}
#[test]
fn format_zero_decimals_with_minor_units() {
let m = Money::new(Decimal::ONE, USD);
assert_eq!(m.format(&locale!("en-US")), "$1.00");
}
#[test]
fn format_zero_decimals_with_no_minor_units() {
let m = Money::new(Decimal::ONE, JPY);
assert_eq!(m.format(&locale!("ja-JP")), "¥1");
}
#[test]
fn format_foreign_currency_in_euro_locales() {
let m = Money::new(Decimal::new(123456789, 2), USD);
assert_eq!(m.format(&locale!("en-US")), "$1,234,567.89");
assert_eq!(
m.format(&locale!("fr-FR")),
"1\u{202f}234\u{202f}567,89\u{a0}$US"
);
assert_eq!(m.format(&locale!("tr-TR")), "$1.234.567,89");
assert_eq!(
m.format(&locale!("pl-PL")),
"1\u{a0}234\u{a0}567,89\u{a0}USD"
);
}
#[test]
fn format_with_options() {
let m = Money::new(Decimal::ONE_HUNDRED, USD);
assert_eq!(
m.format_with_options(
&locale!("en-US"),
FormattingOptions {
decimal_places: 0, rounding_strategy: RoundingStrategy::MidpointNearestEven,
currency_formatter_options: CurrencyFormatterOptions::default(),
}
),
"$100"
);
}
#[test]
fn format_rounding() {
let m = Money::new(Decimal::new(123456750, 2), USD);
assert_eq!(
m.format_with_options(
&locale!("en-US"),
FormattingOptions {
decimal_places: 0, rounding_strategy: RoundingStrategy::MidpointNearestEven,
currency_formatter_options: CurrencyFormatterOptions::default(),
}
),
"$1,234,568" );
assert_eq!(
m.format_with_options(
&locale!("en-US"),
FormattingOptions {
decimal_places: 0, rounding_strategy: RoundingStrategy::MidpointTowardZero,
currency_formatter_options: CurrencyFormatterOptions::default(),
}
),
"$1,234,567" );
}
#[test]
fn format_negative() {
let m = Money::new(Decimal::new(-123456789, 2), USD);
assert_eq!(m.format(&locale!("en-US")), "$-1,234,567.89");
}
}