Money and Currency Handling
How to represent, parse, format, and calculate monetary values accurately across currencies.
Overview
Financial calculations demand precision. Floating-point numbers (float, double) can’t accurately represent most decimal fractions, so rounding errors compound in billing, invoicing, and trading systems. Below you’ll represent money as exact decimal values, format it with locale-aware symbols, do arithmetic without losing precision, and convert between currencies — in Python, JavaScript, and Java.
When to Use
Pull this up when you’re:
- Building e-commerce carts, checkout flows, or invoicing systems. See Input Validation for sanitizing order data.
- Aggregating financial transactions where penny-level accuracy matters
- Displaying prices in USD, EUR, or JPY with correct rounding rules
- Converting between currencies using external exchange rate APIs
Solution
Python
from decimal import Decimal, ROUND_HALF_UP, getcontext
# Set precision for all Decimal operations
getcontext().prec = 28
class Money:
def __init__(self, amount: str | Decimal, currency: str = "USD"):
self.amount = Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
self.currency = currency.upper()
def __add__(self, other: "Money") -> "Money":
if self.currency != other.currency:
raise ValueError("Cannot add different currencies without conversion")
return Money(self.amount + other.amount, self.currency)
def __mul__(self, factor: Decimal) -> "Money":
return Money(self.amount * Decimal(str(factor)), self.currency)
def format(self) -> str:
symbol = {"USD": "$", "EUR": "€", "GBP": "£"}.get(self.currency, self.currency)
return f"{symbol}{self.amount:,}"
@staticmethod
def convert(amount: "Money", rate: Decimal, target_currency: str) -> "Money":
converted = amount.amount * Decimal(str(rate))
return Money(converted, target_currency)
# Usage
price = Money("19.99")
tax = Money("1.70")
total = price + tax
converted = Money.convert(total, Decimal("0.85"), "EUR")
print(total.format()) # $21.69
print(converted.format()) # €18.44
JavaScript
Dinero.js v2 exposes a functional API — amounts live in minor units (cents), so 1999 means $19.99:
import { dinero, add, toFormat, convert } from "dinero.js";
import { USD, EUR } from "@dinero.js/currencies";
const price = dinero({ amount: 1999, currency: USD });
const tax = dinero({ amount: 170, currency: USD });
const total = add(price, tax);
// Conversion with an explicit rate: 1 USD = 0.8500 EUR
const converted = convert(total, EUR, {
EUR: { amount: 8500, scale: 4 },
});
const print = (d) =>
toFormat(d, ({ amount, currency }) => `${currency.code} ${amount.toFixed(2)}`);
console.log(print(total)); // USD 21.69
console.log(print(converted)); // EUR 18.44
Java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.NumberFormat;
import java.util.Currency;
import java.util.Locale;
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, String currencyCode) {
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = Currency.getInstance(currencyCode);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), currency.getCurrencyCode());
}
public Money multiply(BigDecimal factor) {
return new Money(this.amount.multiply(factor), currency.getCurrencyCode());
}
public String format(Locale locale) {
NumberFormat formatter = NumberFormat.getCurrencyInstance(locale);
formatter.setCurrency(currency);
return formatter.format(amount);
}
public static Money convert(Money source, BigDecimal rate, String targetCurrency) {
BigDecimal converted = source.amount.multiply(rate);
return new Money(converted, targetCurrency);
}
// Usage
public static void main(String[] args) {
Money price = new Money(new BigDecimal("19.99"), "USD");
Money tax = new Money(new BigDecimal("1.70"), "USD");
Money total = price.add(tax);
Money eur = Money.convert(total, new BigDecimal("0.85"), "EUR");
System.out.println(total.format(Locale.US)); // $21.69
System.out.println(eur.format(Locale.GERMANY)); // 18,44 €
}
}
Explanation
- Never use
floatordoublefor money — binary floating-point can’t represent values like0.1exactly, causing0.1 + 0.2 != 0.3errors that accumulate in financial systems. - Use integer minor units (cents) or arbitrary-precision decimals (
Decimal,BigDecimal, Dinero.js) to keep exact values throughout calculations. - Rounding must happen explicitly at defined boundaries (per-line item, per-tax, and per-total), never implicitly through floating-point error.
- Currency conversion should use a fetched exchange rate, apply it with exact arithmetic, then round to the target currency’s minor units.
- Locale formatting separates display logic from storage: store raw numeric values and format with
NumberFormator ICU libraries for symbols, separators, and placement. - Splitting money needs an allocation strategy — dividing $100 three ways yields $33.33/$33.33/$33.34, not three equal parts. Dinero.js
allocateand Java’sdivideAndRemainderdistribute the leftover pennies deterministically; naive division loses them.
Every money operation follows the same pipeline — parse, validate, compute, convert, round, format:
Working with Exchange Rates
Rates change constantly, so the conversion code matters less than the policy around it:
- Fetch the rate once per transaction — pull it from your exchange rate provider and reuse that exact rate for every line in the order, never re-fetch mid-calculation.
- Store the rate you used — keep
rate,rate_timestamp, andtarget_amounton the transaction record. Auditors and support tickets will ask “why €18.44?” months later. - Cache aggressively, with a TTL — most providers update daily; a cached rate with a few hours of TTL is fine for storefronts but not for settlement.
- Round only after converting —
21.69 × 0.85 = 18.4365becomes18.44at the end, never rounded at intermediate steps.
Variants
| Approach | Library / Type | Best For |
|---|---|---|
| Integer cents | long / bigint | High-frequency paths, smallest memory footprint |
| Arbitrary precision | Decimal (Python), BigDecimal (Java) | General-purpose exact decimal math |
| Money library | Dinero.js, Money PHP, JSR-354 (Java) | Rich formatting, allocation, and comparison APIs |
| Database storage | DECIMAL(19,4) (SQL), NUMERIC (PostgreSQL) | Persistent exact values with 4-decimal precision for rates |
What Works
- Store amounts in minor units (cents) or exact decimal types; never store money as floating-point in databases.
- Round at the right boundary — calculate line items with full precision, round per-line, then sum rounded values for the total.
- Separate money from display — keep raw
Decimal/BigDecimal/ cents internally and format only at the UI/API layer. - Know your rounding mode —
HALF_UPis standard for commerce;HALF_EVEN(banker’s rounding, Python’s default) reduces bias across large aggregations. Pick one per domain and write down which — auditors always ask. - Cache exchange rates with TTL and timestamp; always convert using the rate that applied at the transaction time.
Common Mistakes
- Reaching for
floatordoublefor prices — that’s how0.30000000000000004ends up on an invoice. - Rounding only at the very end of a long calculation chain, propagating sub-penny errors.
- Storing money as strings and parsing with locale-dependent commas/dots, causing regional bugs.
- Adding different currencies directly without conversion, producing meaningless totals.
- Ignoring currency subunits — some currencies (JPY, KRW) have no decimal places; others (BHD, IQD) have 3.
When Not to Use
- Display-only estimates — percentages, progress bars, and rough projections where nobody reconciles the last cent tolerate
floatfine. - Prototypes and internal tools — if no real payment touches the number, the ceremony of a Money type buys little.
- When your framework already ships a money type — Django’s money fields,
money-rails, JSR-354 implementations — use it. A hand-rolled class is yours to maintain forever.
Testing Money Code
Money bugs hide at boundaries, so test the edges rather than the happy path:
- Rounding boundaries —
19.995rounds to20.00under HALF_UP;2.5rounds to2under HALF_EVEN. Write a test for the mode you chose, not the one you assumed. - Currency mismatch — adding USD to EUR must raise, not silently sum.
- Allocation round-trips — splitting
100.00three ways must return parts that re-sum to100.00. - Minor-unit edge cases — JPY (
¥1234, zero decimals) and BHD (three decimals) break code that hard-codesquantize("0.01"). - Idempotent conversions — converting USD→EUR→USD rarely returns the original amount; assert the documented tolerance instead of equality.
Further Reading
- Python
decimalmodule docs — how contexts,quantize, and rounding modes actually work. - Dinero.js documentation — the functional money API the example above uses.
- JSR-354 (JavaMoney) specification — the
javax.moneystandard most Java money libraries implement. - ISO 4217 currency codes — where each currency’s official minor unit is defined.
Frequently Asked Questions
Why can't I just use float or double for money?
Binary floating-point stores fractions as sums of inverse powers of 2. Decimals like 0.1 are infinite repeating sequences in binary, so they get rounded. These tiny errors accumulate in multiplication and division, causing invoices to be off by pennies or worse.
How should I store money in a database?
Use DECIMAL(19,4) (or NUMERIC in PostgreSQL) to preserve 4 decimal places of precision. This handles sub-penny calculations (tax rates, exchange rates) while keeping the final 2-decimal value exact. Avoid FLOAT, DOUBLE, and REAL columns.
What rounding mode should I use?
HALF_UP (round 0.5 away from zero) is standard for most commercial applications. HALF_EVEN (banker's rounding) is used in some financial standards and reduces bias across large aggregations. Always round consistently within your domain and document the mode for auditors.
How do I split a payment without losing cents?
Divide into minor units and distribute the remainder deterministically. $100.00 split three ways becomes 3333 + 3333 + 3334 cents. Dinero.js has allocate for exactly this; with plain decimals, sort largest-remainder first so the same split always produces the same parts.
What about currencies without decimals like JPY?
Store the minor unit count, not a fixed 2-decimal assumption. JPY and KRW have zero decimal places, BHD and IQD have three — ISO 4217 defines the exponent per currency. Code that hard-codes 100 as the divisor corrupts values the first time a ¥ price flows through.
Decimal type or integer cents — which should I pick?
Integer cents (1999 for $19.99) are fast, index-friendly, and portable across languages, but every boundary needs conversion discipline. Decimal/BigDecimal reads more naturally and handles sub-unit math (rates, tax percentages) without extra thought. Pick cents for hot paths and simple ledgers, decimals for general business logic.
Related Resources
Date Formatting in Python, JavaScript & Java
How to parse, format, and display dates across timezones in Python, JavaScript, and Java, with a per-language directive cheat sheet.
RecipeInput Validation: Schema-First Examples in 3 Languages
Validate user input at the API boundary with schema libraries. Pydantic, Zod, and Jakarta Validation examples with sanitization and structured errors.
RecipeCall a REST API: Python, JavaScript, Java & Go Examples
How to make HTTP requests to a REST API and handle the JSON response in Python, JavaScript, Java, and Go.
RecipeCaching & Memoization in Python, JavaScript, and Java
How to cache expensive computations and API responses using in-memory LRU, TTL, and distributed caches across Python, JavaScript, and Java.
RecipeParse JSON in Python, JavaScript and Java
Parse JSON strings into native objects in Python, JavaScript and Java — with error handling, streaming for large files and schema validation.