rust 136 lines · 3 tabs

Locale-Aware Money Formatting With Minor Units in Rust

Shared by codesnips Aug 2026
3 tabs
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Currency {
    pub code: &'static str,
    pub symbol: &'static str,
    pub exponent: u32,
}

impl Currency {
    pub const USD: Currency = Currency { code: "USD", symbol: "$", exponent: 2 };
    pub const EUR: Currency = Currency { code: "EUR", symbol: "\u{20AC}", exponent: 2 };
    pub const JPY: Currency = Currency { code: "JPY", symbol: "\u{A5}", exponent: 0 };
}

#[derive(Debug, PartialEq)]
pub enum MoneyError {
    CurrencyMismatch,
    Overflow,
    Parse(String),
}

#[derive(Debug, Clone)]
pub struct Money {
    pub minor_units: i64,
    pub currency: Currency,
}

impl Money {
    pub fn from_major(major: i64, currency: Currency) -> Money {
        let scale = 10i64.pow(currency.exponent);
        Money { minor_units: major * scale, currency }
    }

    pub fn checked_add(&self, other: &Money) -> Result<Money, MoneyError> {
        if self.currency != other.currency {
            return Err(MoneyError::CurrencyMismatch);
        }
        let sum = self
            .minor_units
            .checked_add(other.minor_units)
            .ok_or(MoneyError::Overflow)?;
        Ok(Money { minor_units: sum, currency: self.currency.clone() })
    }
}
3 files · rust Explain with highlit

This snippet models money the way payment systems actually do — as an integer count of a currency's smallest unit (minor units) rather than a floating-point amount — and then renders those amounts for display according to a locale. Storing money as f64 invites rounding drift and comparison bugs, so the Money type tab keeps an i64 of minor units alongside a Currency, exposing arithmetic that stays exact.

In Money type, Currency carries the ISO code, the symbol, and exponent, the number of decimal places the currency uses (2 for USD, 0 for JPY, 3 for BHD). Money::from_major multiplies by 10^exponent so that 12.34 USD becomes 1234 minor units, and checked_add refuses to combine mismatched currencies, returning a MoneyError instead of silently producing nonsense. Keeping the exponent on the currency is what lets the same code handle zero-decimal and three-decimal currencies without special cases.

The LocaleFormat tab separates value from presentation. A Locale describes the grouping separator, decimal separator, and whether the symbol leads or trails — the difference between $1,234.50 and 1 234,50 €. format splits the minor units into whole and fractional parts using the currency's exponent, groups the integer part in threes via group_digits, and reattaches the fraction with the correct separator. Negative amounts are handled by formatting the absolute value and prefixing a sign, which avoids the classic bug of a stray - landing inside the grouped digits.

The resolve and print tab ties it together: it parses raw major-unit strings against a Currency, sums them with checked_add, and formats the total under two different Locales to show the same amount rendered as US English and German. The trade-off of this design is a little upfront ceremony — everything is explicit exponents and checked arithmetic — in exchange for exactness and predictable rendering. A developer reaches for this pattern whenever money crosses storage, computation, and display boundaries, where f64 and ad-hoc string formatting eventually cause real financial discrepancies.


Related snips

Share this code

Here's the card — post it anywhere.

Locale-Aware Money Formatting With Minor Units in Rust — share card
Link copied