use std::fmt::{self, Write as _};
use crate::{Money, RoundingMode};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Format {
identifier: Identifier,
position: Option<Position>,
spaced: Option<bool>,
negative: NegativeStyle,
precision: Option<(u32, RoundingMode)>,
grouping: &'static [u8],
group_separator: char,
decimal_separator: char,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Identifier {
Code,
Symbol,
None,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Position {
Prefix,
Suffix,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum NegativeStyle {
Minus,
Parentheses,
}
impl Format {
#[must_use]
pub const fn new() -> Self {
Self {
identifier: Identifier::Code,
position: None,
spaced: None,
negative: NegativeStyle::Minus,
precision: None,
grouping: &[3],
group_separator: ',',
decimal_separator: '.',
}
}
#[must_use]
pub const fn code(mut self) -> Self {
self.identifier = Identifier::Code;
self
}
#[must_use]
pub const fn symbol(mut self) -> Self {
self.identifier = Identifier::Symbol;
self
}
#[must_use]
pub const fn amount_only(mut self) -> Self {
self.identifier = Identifier::None;
self
}
#[must_use]
pub const fn prefix(mut self) -> Self {
self.position = Some(Position::Prefix);
self
}
#[must_use]
pub const fn suffix(mut self) -> Self {
self.position = Some(Position::Suffix);
self
}
#[must_use]
pub const fn spaced(mut self) -> Self {
self.spaced = Some(true);
self
}
#[must_use]
pub const fn no_space(mut self) -> Self {
self.spaced = Some(false);
self
}
#[must_use]
pub const fn minus_sign(mut self) -> Self {
self.negative = NegativeStyle::Minus;
self
}
#[must_use]
pub const fn parentheses(mut self) -> Self {
self.negative = NegativeStyle::Parentheses;
self
}
#[must_use]
pub const fn precision(mut self, digits: u32, mode: RoundingMode) -> Self {
self.precision = Some((digits, mode));
self
}
#[must_use]
pub const fn grouping(mut self, pattern: &'static [u8]) -> Self {
self.grouping = pattern;
self
}
#[must_use]
pub const fn no_grouping(mut self) -> Self {
self.grouping = &[];
self
}
#[must_use]
pub const fn separators(mut self, group: char, decimal: char) -> Self {
self.group_separator = group;
self.decimal_separator = decimal;
self
}
}
impl Default for Format {
fn default() -> Self {
Self::new()
}
}
impl Money {
#[must_use]
pub fn format_with(&self, format: &Format) -> impl fmt::Display + use<> {
Formatted {
money: *self,
format: *format,
}
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.format_with(&Format::default()), f)
}
}
struct Formatted {
money: Money,
format: Format,
}
impl fmt::Display for Formatted {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_padded(f, &self.render())
}
}
pub(crate) fn write_padded(f: &mut fmt::Formatter<'_>, rendered: &str) -> fmt::Result {
let Some(width) = f.width() else {
return f.write_str(rendered);
};
let length = rendered.chars().count();
if length >= width {
return f.write_str(rendered);
}
let padding = width - length;
let (left, right) = match f.align().unwrap_or(fmt::Alignment::Right) {
fmt::Alignment::Left => (0, padding),
fmt::Alignment::Right => (padding, 0),
fmt::Alignment::Center => (padding / 2, padding - padding / 2),
};
let fill = f.fill();
for _ in 0..left {
f.write_char(fill)?;
}
f.write_str(rendered)?;
for _ in 0..right {
f.write_char(fill)?;
}
Ok(())
}
impl Formatted {
fn render(&self) -> String {
let currency = self.money.currency();
let format = &self.format;
let amount = match format.precision {
Some((digits, mode)) => self.money.round(digits, mode).amount(),
None => self.money.amount(),
};
let negative = amount.is_sign_negative() && !amount.is_zero();
let fraction_digits = match format.precision {
Some((digits, _)) => digits,
None => amount.scale().max(currency.minor_digits()),
} as usize;
let magnitude = amount.abs();
let scale = magnitude.scale() as usize;
let mut digits = magnitude.mantissa().to_string();
while digits.len() <= scale {
digits.insert(0, '0');
}
let (integer, fraction) = digits.split_at(digits.len() - scale);
let mut number = group_digits(integer, format.grouping, format.group_separator);
if fraction_digits > 0 {
number.push(format.decimal_separator);
number.push_str(fraction);
for _ in fraction.len()..fraction_digits {
number.push('0');
}
}
let code = currency.alphabetic_code();
let (identifier, position, spaced) = match format.identifier {
Identifier::Code => (
Some(code.as_str()),
format.position.unwrap_or(Position::Suffix),
format.spaced.unwrap_or(true),
),
Identifier::Symbol => (
Some(currency.symbol()),
format.position.unwrap_or(Position::Prefix),
format.spaced.unwrap_or(false),
),
Identifier::None => (None, Position::Prefix, false),
};
let mut out = String::new();
let parenthesized = negative && matches!(format.negative, NegativeStyle::Parentheses);
if parenthesized {
out.push('(');
} else if negative {
out.push('-');
}
match (identifier, position) {
(Some(identifier), Position::Prefix) => {
out.push_str(identifier);
if spaced {
out.push(' ');
}
out.push_str(&number);
}
(Some(identifier), Position::Suffix) => {
out.push_str(&number);
if spaced {
out.push(' ');
}
out.push_str(identifier);
}
(None, _) => out.push_str(&number),
}
if parenthesized {
out.push(')');
}
out
}
}
fn group_digits(digits: &str, pattern: &[u8], separator: char) -> String {
let mut groups = Vec::new();
let mut rest = digits.as_bytes();
let mut sizes = pattern.iter().copied();
let mut size = sizes.next().unwrap_or(0) as usize;
while size != 0 && rest.len() > size {
let (head, group) = rest.split_at(rest.len() - size);
groups.push(group);
rest = head;
size = sizes.next().map_or(size, |s| s as usize);
}
let ascii = |bytes| std::str::from_utf8(bytes).expect("digits are ASCII");
let mut out = String::from(ascii(rest));
for group in groups.into_iter().rev() {
out.push(separator);
out.push_str(ascii(group));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Currency;
use rust_decimal::prelude::*;
#[test]
fn default_format_test() {
let money = Money::from_minor(150000, &Currency::USD);
assert_eq!(money.to_string(), "1,500.00 USD");
}
#[test]
fn pads_fraction_to_minor_digits_test() {
assert_eq!(
Money::from_major(-3, &Currency::USD).to_string(),
"-3.00 USD"
);
}
#[test]
fn zero_minor_digits_omit_decimal_mark_test() {
assert_eq!(Money::from_major(5, &Currency::JPY).to_string(), "5 JPY");
}
#[test]
fn excess_precision_is_shown_in_full_test() {
let money = Money::from_decimal(dec!(1.2345), &Currency::USD);
assert_eq!(money.to_string(), "1.2345 USD");
}
#[test]
fn symbol_test() {
let money = Money::from_minor(150000, &Currency::USD);
assert_eq!(
money.format_with(&Format::new().symbol()).to_string(),
"$1,500.00"
);
}
#[test]
fn symbol_suffix_spaced_test() {
let money = Money::from_minor(150, &Currency::USD);
let format = Format::new().symbol().suffix().spaced();
assert_eq!(money.format_with(&format).to_string(), "1.50 $");
}
#[test]
fn code_prefix_test() {
let money = Money::from_minor(150000, &Currency::USD);
let format = Format::new().prefix();
assert_eq!(money.format_with(&format).to_string(), "USD 1,500.00");
}
#[test]
fn code_no_space_test() {
let money = Money::from_minor(150, &Currency::USD);
let format = Format::new().no_space();
assert_eq!(money.format_with(&format).to_string(), "1.50USD");
}
#[test]
fn amount_only_test() {
let money = Money::from_minor(150000, &Currency::USD);
assert_eq!(
money.format_with(&Format::new().amount_only()).to_string(),
"1,500.00"
);
}
#[test]
fn negative_symbol_test() {
let money = Money::from_minor(-150000, &Currency::USD);
assert_eq!(
money.format_with(&Format::new().symbol()).to_string(),
"-$1,500.00"
);
}
#[test]
fn parentheses_test() {
let format = Format::new().symbol().parentheses();
let negative = Money::from_minor(-150000, &Currency::USD);
let positive = Money::from_minor(150000, &Currency::USD);
assert_eq!(negative.format_with(&format).to_string(), "($1,500.00)");
assert_eq!(positive.format_with(&format).to_string(), "$1,500.00");
}
#[test]
fn minus_sign_restores_default_test() {
let money = Money::from_minor(-150, &Currency::USD);
let format = Format::new().parentheses().minus_sign();
assert_eq!(money.format_with(&format).to_string(), "-1.50 USD");
}
#[test]
fn precision_rounds_test() {
let money = Money::from_decimal(dec!(2.675), &Currency::USD);
assert_eq!(
money
.format_with(&Format::new().precision(2, RoundingMode::HalfUp))
.to_string(),
"2.68 USD"
);
assert_eq!(
money
.format_with(&Format::new().precision(2, RoundingMode::HalfDown))
.to_string(),
"2.67 USD"
);
}
#[test]
fn precision_pads_test() {
let money = Money::from_major(1, &Currency::USD);
let format = Format::new().precision(4, RoundingMode::HalfEven);
assert_eq!(money.format_with(&format).to_string(), "1.0000 USD");
}
#[test]
fn precision_zero_digits_test() {
let money = Money::from_decimal(dec!(1500.5), &Currency::USD);
let format = Format::new().precision(0, RoundingMode::HalfUp);
assert_eq!(money.format_with(&format).to_string(), "1,501 USD");
}
#[test]
fn rounded_to_zero_shows_no_sign_test() {
let money = Money::from_decimal(dec!(-0.004), &Currency::USD);
assert_eq!(
money
.format_with(&Format::new().precision(2, RoundingMode::HalfEven))
.to_string(),
"0.00 USD"
);
assert_eq!(
money
.format_with(
&Format::new()
.parentheses()
.precision(2, RoundingMode::HalfEven)
)
.to_string(),
"0.00 USD"
);
}
#[test]
fn indian_grouping_test() {
let money = Money::from_decimal(dec!(12345678.90), &Currency::INR);
let format = Format::new().grouping(&[3, 2]);
assert_eq!(money.format_with(&format).to_string(), "1,23,45,678.90 INR");
}
#[test]
fn no_grouping_test() {
let money = Money::from_major(1234567, &Currency::USD);
let format = Format::new().no_grouping();
assert_eq!(money.format_with(&format).to_string(), "1234567.00 USD");
}
#[test]
fn separators_test() {
let money = Money::from_minor(150000, &Currency::EUR);
let format = Format::new().separators('.', ',');
assert_eq!(money.format_with(&format).to_string(), "1.500,00 EUR");
}
#[test]
fn width_and_alignment_test() {
let money = Money::from_minor(150, &Currency::USD);
let bare = Format::new().amount_only();
assert_eq!(format!("{:8}", money.format_with(&bare)), " 1.50");
assert_eq!(format!("{:<8}", money.format_with(&bare)), "1.50 ");
assert_eq!(format!("{:*^8}", money.format_with(&bare)), "**1.50**");
}
#[test]
fn display_honors_width_test() {
let money = Money::from_minor(150, &Currency::USD);
assert_eq!(format!("{money:>12}"), " 1.50 USD");
}
#[test]
fn fmt_precision_flag_is_ignored_test() {
let money = Money::from_minor(150, &Currency::USD);
assert_eq!(format!("{money:.1}"), "1.50 USD");
}
}