Money has one rule that matters more than any other: never store it
in a float. 0.1 + 0.2 isn't 0.3, and across a
few hundred transactions that error becomes real lost cents. SCUA's
answer is a first-class money type: an
exact amount that carries its currency with it. You write it as an
amount and a currency code, and it does what money should. The
arithmetic stays exact, currencies won't silently mix, and the totals
add up.
(This is the tool for real-world money. Game counters like gold and
gems are just an int; the huge "currencies" in idle games
are a different thing again — see numbers. This page is about exact amounts
with a currency.)
#Write it as an amount and a currency
A money literal is a number followed by a three-letter ISO 4217 currency code:
let price = 19.99 USD
let fee = 500 JPY
let vat = 12.50 EUR
print(price) -- $19.99
print(fee) -- ¥500 (yen has no minor unit)
The amount is exact — 19.99 here is read as an exact
decimal, not the binary float 19.99 would be on its own.
The currency travels with the value, so an amount always knows what it
is, and printing picks the right symbol and number of places for that
currency.
For a currency you only know at run time, build one with
money.of:
import money
let owed = money.of(amount, code) -- amount is a decimal/int, code a string like "USD"
An unknown currency code, or more decimal places than the currency
allows (100.5 JPY — yen has none), is an error you find
right away, not a wrong number later.
#Arithmetic that can't quietly go wrong
Money is a quantity of a currency, and the arithmetic follows from that. A plain number is a scalar (a count, or a rate, or a factor), so you multiply and divide money by numbers, and add money only to money of the same currency:
let unit = 400 USD
let gross = unit * 3 -- $1,200.00 (three at $400 each)
let tax = gross * 0.08d -- $96.00 (8% of the total)
let total = gross + 96 USD -- $1,296.00 (money + money, same currency)
Dividing money by money cancels the currency and gives you a plain ratio — handy for margins and percentages — while dividing money by a number gives you a share:
print(80 USD / 20 USD) -- 4.000000 (a plain number — how many $20s are in $80)
print(100 USD / 3) -- $33.33 (one rounded third)
Everything that shouldn't have a meaning is caught, most of it before the program even runs:
price + 5 -- error: cannot add money and a plain number — make it money first, e.g. `5 USD`
100 USD + 100 EUR -- error: cannot add different currencies (USD and EUR)
price * price -- error: cannot multiply money by money — scale by a quantity or rate
100 USD < 50 EUR -- error: cannot order different currencies
Adding a bare number to money, multiplying two amounts, or mixing currencies is never a silent wrong total. Each one is a clear error with the fix written into it. The only check that waits for run time is which currency two amounts actually are; everything else the compiler catches for you.
#Formatting, splitting, rounding, converting
The money module handles the currency-specific jobs.
Because a money value already carries its currency,
money.format needs nothing else:
import money
print(money.format(1234.5 USD)) -- $1,234.50
print(money.format(1234.5 EUR)) -- €1,234.50
print(money.format(1234 JPY)) -- ¥1,234
print(money.format(-742.5 USD)) -- -$742.50
Splitting a bill evenly is the classic place naive code drops or
invents a cent. money.split hands back shares that sum back
to exactly the total:
print(money.split(100 USD, 3)) -- [$33.34, $33.33, $33.33]
The leftover cent goes to the first share rather than vanishing.
money.round rounds a computed amount to the currency's
places (banker's rounding, the standard for money), and
money.convert changes currency at a rate you supply.
There's no hidden exchange rate anywhere:
print(money.round(100 USD / 3)) -- $33.33
print(money.convert(100 USD, "EUR", 0.92d)) -- €92.00
#Money in a table
Because the currency rides along with each value, a column of money stays money through a whole calculation. Read a CSV, total a column, group by a key, and the answer comes back exact and currency-tagged. A column that accidentally mixes currencies stops with a clear message instead of adding dollars to euros:
let sales = csv("region,amount
west,400.00 USD
east,150.00 USD
west,50.00 USD")
print(sales.total("amount")) -- $600.00
print(sales.group_sum("region", "amount").to_table())
┌────────┬─────────┐
│ region │ total │
├────────┼─────────┤
│ west │ $450.00 │
│ east │ $150.00 │
└────────┴─────────┘
That's a spreadsheet's worth of work in three lines, and it's exact, currency-aware, and loud about a mixed-currency mistake. See summarize and query data for more on tables.
#The short version
- Write money as an amount and a currency:
19.99 USD. It's exact, never a float, and the currency travels with it. - Multiply or divide money by a number (a quantity or rate); add money only to money of the same currency. Everything else is an error, most of it at compile time.
money.formatfor display,money.splitfor penny-exact shares,money.roundfor a settled amount,money.convertfor an explicit-rate conversion.- In a
frame, a money column stays money —totalandgroup_sumgive exact, currency-tagged answers, and a mixed-currency column fails loudly. - Build one dynamically with
money.of(amount, code).