use crate::Currency;
use crate::MoneyError;
use crate::{BaseMoney, Decimal};
const ESCAPE_SYMBOL: char = '\\';
const AMOUNT_FORMAT_SYMBOL: char = 'a';
const CODE_FORMAT_SYMBOL: char = 'c';
const SYMBOL_FORMAT_SYMBOL: char = 's';
const MINOR_FORMAT_SYMBOL: char = 'm';
const NEGATIVE_FORMAT_SYMBOL: char = 'n';
pub(crate) static FORMAT_SYMBOLS: &[char] = &[
'a', 'c', 's', 'm', 'n', ];
pub(crate) const CODE_FORMAT: &str = "c na"; pub(crate) const SYMBOL_FORMAT: &str = "nsa";
pub(crate) const CODE_FORMAT_MINOR: &str = "c na m"; pub(crate) const SYMBOL_FORMAT_MINOR: &str = "nsa m";
pub(crate) fn format<C: Currency>(money: &impl BaseMoney<C>, format_str: &str) -> String {
format_with_separator(
money,
format_str,
C::THOUSAND_SEPARATOR,
C::DECIMAL_SEPARATOR,
)
}
pub(crate) fn format_128_abs(num: i128, thousand_separator: &str) -> String {
let abs_num = num.abs();
let num_str = abs_num.to_string();
let mut result = String::new();
let len = num_str.len();
for (i, ch) in num_str.chars().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
result.push_str(thousand_separator);
}
result.push(ch);
}
result
}
pub(crate) fn format_decimal_abs(
decimal: Decimal,
thousand_separator: &str,
decimal_separator: &str,
minor_unit: u16,
) -> String {
let abs_decimal = decimal.abs();
let decimal_str = abs_decimal.to_string();
let parts: Vec<&str> = decimal_str.split('.').collect();
let integer_part = parts[0];
let fractional_part = parts.get(1);
let mut result = String::new();
let len = integer_part.len();
for (i, ch) in integer_part.chars().enumerate() {
if i > 0 && (len - i).is_multiple_of(3) {
result.push_str(thousand_separator);
}
result.push(ch);
}
if let Some(frac) = fractional_part {
result.push_str(decimal_separator);
if frac.len() >= minor_unit.into() {
result.push_str(frac);
} else {
result.push_str(frac);
let frac_len = frac.len();
let minor_unit_len: usize = minor_unit.into();
let remaining_frac_len = minor_unit_len - frac_len;
result.push_str(&"0".repeat(remaining_frac_len));
}
} else if minor_unit > 0 {
result.push_str(decimal_separator);
result.push_str(&"0".repeat(minor_unit.into()));
}
result
}
pub(crate) fn format_with_separator<C: Currency>(
money: &impl BaseMoney<C>,
format_str: &str,
thousand_separator: &str,
decimal_separator: &str,
) -> String {
format_with_separator_internal(
C::CODE,
C::SYMBOL,
C::MINOR_UNIT,
C::MINOR_UNIT_SYMBOL,
money.is_negative(),
money.amount(),
money.minor_amount(),
format_str,
thousand_separator,
decimal_separator,
)
}
#[allow(clippy::too_many_arguments)]
#[inline]
pub(crate) fn format_with_separator_internal(
code: &str,
symbol: &str,
minor_unit: u16,
minor_unit_symbol: &str,
is_negative: bool,
amount: Decimal,
minor_amount: Option<i128>,
format_str: &str,
thousand_separator: &str,
decimal_separator: &str,
) -> String {
let display_amount = if contains_active_format_symbol(format_str, MINOR_FORMAT_SYMBOL) {
if let Some(minor_amount) = minor_amount {
format_128_abs(minor_amount, thousand_separator)
} else {
"OVERFLOWED".into()
}
} else {
format_decimal_abs(amount, thousand_separator, decimal_separator, minor_unit)
};
format_with_amount_internal(
code,
symbol,
minor_unit_symbol,
&display_amount,
is_negative,
format_str,
)
}
fn contains_active_format_symbol(format_str: &str, symbol: char) -> bool {
let mut chars = format_str.chars().peekable();
while let Some(ch) = chars.next() {
if ch == ESCAPE_SYMBOL {
if let Some(&next_ch) = chars.peek() {
if next_ch == '{' {
chars.next(); for inner_ch in chars.by_ref() {
if inner_ch == '}' {
break;
}
}
} else {
chars.next();
}
}
} else if ch == symbol {
return true;
}
}
false
}
pub(crate) fn format_with_amount<C: Currency>(
display_amount: &str,
is_negative: bool,
format_str: &str,
) -> String {
format_with_amount_internal(
C::CODE,
C::SYMBOL,
C::MINOR_UNIT_SYMBOL,
display_amount,
is_negative,
format_str,
)
}
#[inline]
pub(crate) fn format_with_amount_internal(
code: &str,
symbol: &str,
minor_unit_symbol: &str,
display_amount: &str,
is_negative: bool,
format_str: &str,
) -> String {
let mut chars = format_str.chars().peekable();
let mut result = String::new();
while let Some(ch) = chars.next() {
if ch == ESCAPE_SYMBOL {
if let Some(&next_ch) = chars.peek() {
if next_ch == '{' {
chars.next(); for inner_ch in chars.by_ref() {
if inner_ch == '}' {
break;
}
result.push(inner_ch);
}
continue;
} else if FORMAT_SYMBOLS.contains(&next_ch) || next_ch == ESCAPE_SYMBOL {
chars.next();
result.push(next_ch);
continue;
} else {
result.push(ch);
}
} else {
result.push(ch);
}
} else {
match ch {
AMOUNT_FORMAT_SYMBOL => result.push_str(display_amount),
CODE_FORMAT_SYMBOL => result.push_str(code),
SYMBOL_FORMAT_SYMBOL => result.push_str(symbol),
MINOR_FORMAT_SYMBOL => result.push_str(minor_unit_symbol),
NEGATIVE_FORMAT_SYMBOL => {
if is_negative {
result.push('-');
}
}
' ' => result.push(' '),
_ => result.push(ch),
}
}
}
result
}
#[cfg(feature = "locale")]
pub(crate) fn format_locale_amount<C: Currency>(
money: &impl BaseMoney<C>,
locale_str: &str,
format_str: &str,
) -> Result<String, MoneyError> {
use crate::MoneyError;
use crate::fmt::format_with_amount;
use icu_decimal::{DecimalFormatter, input::Decimal as LocaleDecimal};
use icu_locale::Locale;
let loc: Locale = locale_str.parse().map_err(|_| {
MoneyError::ParseLocaleError(
format!(
"failed parsing locale {} , invalid or not found",
locale_str
)
.into(),
)
})?;
let formatter = DecimalFormatter::try_new(loc.into(), Default::default())
.map_err(|_| MoneyError::ParseLocaleError("failed initiating decimal formatter".into()))?;
let is_negative = money.is_negative();
let curr_minor_unit = C::MINOR_UNIT.into();
let abs_amount = if money.scale() < curr_minor_unit {
let remaining_scale: usize = (curr_minor_unit - money.scale())
.try_into()
.map_err(|_| MoneyError::ParseLocaleError("invalid minor unit".into()))?;
let minor_amount = "0".repeat(remaining_scale);
let fract = if money.scale() == 0 {
format!(".{}", minor_amount)
} else {
minor_amount
};
let mut ret = money.amount().abs().to_string();
ret.push_str(&fract);
ret
} else {
money.amount().abs().to_string()
};
let decimal = LocaleDecimal::try_from_str(&abs_amount).map_err(|_| {
MoneyError::ParseLocaleError(
format!("failed parsing {} into locale decimal", &abs_amount).into(),
)
})?;
let formatted_decimal = formatter.format(&decimal).to_string();
let ret = format_with_amount::<C>(&formatted_decimal, is_negative, format_str);
Ok(ret)
}