#[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() })
}
}
use crate::money::Money;
pub struct Locale {
pub grouping: char,
pub decimal: char,
pub symbol_leads: bool,
}
impl Locale {
pub fn en_us() -> Locale {
Locale { grouping: ',', decimal: '.', symbol_leads: true }
}
pub fn de_de() -> Locale {
Locale { grouping: '.', decimal: ',', symbol_leads: false }
}
pub fn format(&self, money: &Money) -> String {
let exp = money.currency.exponent;
let scale = 10i64.pow(exp);
let negative = money.minor_units < 0;
let abs = money.minor_units.unsigned_abs();
let whole = abs / scale as u64;
let frac = abs % scale as u64;
let mut number = group_digits(whole, self.grouping);
if exp > 0 {
number.push(self.decimal);
number.push_str(&format!("{:0width$}", frac, width = exp as usize));
}
let sym = money.currency.symbol;
let body = if self.symbol_leads {
format!("{}{}", sym, number)
} else {
format!("{}\u{00A0}{}", number, sym)
};
if negative {
format!("-{}", body)
} else {
body
}
}
}
fn group_digits(value: u64, sep: char) -> String {
let digits = value.to_string();
let bytes = digits.as_bytes();
let mut out = String::new();
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i) % 3 == 0 {
out.push(sep);
}
out.push(*b as char);
}
out
}
mod money;
mod locale;
use locale::Locale;
use money::{Currency, Money, MoneyError};
fn parse_major(raw: &str, currency: Currency) -> Result<Money, MoneyError> {
let value: i64 = raw
.trim()
.parse()
.map_err(|_| MoneyError::Parse(raw.to_string()))?;
Ok(Money::from_major(value, currency))
}
fn main() -> Result<(), MoneyError> {
let lines = ["1200", "34", "5"];
let mut total = Money::from_major(0, Currency::EUR);
for line in lines {
let item = parse_major(line, Currency::EUR)?;
total = total.checked_add(&item)?;
}
let us = Locale::en_us();
let de = Locale::de_de();
println!("US: {}", us.format(&total));
println!("DE: {}", de.format(&total));
let refund = Money { minor_units: -4599, currency: Currency::USD };
println!("US: {}", us.format(&refund));
Ok(())
}
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
struct Config<'a> {
name: &'a str,
value: &'a str,
}
fn parse_config(line: &str) -> Config {
Lifetime annotations for flexible borrowing in structs
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about)]
struct Args {
#[arg(short, long)]
clap for CLI argument parsing with derive macros
macro_rules! create_function {
($func_name:ident) => {
fn $func_name() {
println!("Called {}", stringify!($func_name));
}
};
Declarative macros (macro_rules!) for code generation
Share this code
Here's the card — post it anywhere.