money holds an amount of money exactly, so the
pennies always add up. The trick is simple: instead of storing $19.99 as the
fraction 19.99, it stores it as the whole number 1999 — a
count of cents. A computer can hold whole numbers perfectly. It cannot hold
fractions of a dollar exactly, and the tiny errors that creep in from those
fractions are exactly what makes a total come out a penny wrong. By keeping
everything in whole cents, money sidesteps that entirely. You
build amounts from dollars or from a typed-in string, add and subtract and
compare them, multiply by a rate (tax, interest) with the rounding made
explicit, split a sum into even shares without losing or inventing a penny,
and format the result back into a familiar $12.34. Everything is
pure — each word hands you back a new Money and
never changes the one you gave it.
Ledgers never had a rounding problem. A Victorian clerk kept accounts in
pounds, shillings and pence because money is a count — of
coins, ultimately — and counts, added down a column, always balance to
the penny. Computing broke that guarantee by an accident of base. Binary
floating point — the base-two number format computers calculate in
— cannot hold one tenth exactly, for the same reason decimal notation
cannot write a third. 0.1 in a double is a very close lie, and a
few million additions of a very close lie is a total that is visibly wrong.
Business computing knew this from the start: COBOL gave money its own decimal
arithmetic in 1959 rather than trust the engineering formats. And the
folklore of fractions of a cent quietly skimmed into someone's account is as
old as payroll programs. The remedy has never changed. Keep money as an
integer count of its smallest unit, and make every rounding a decision
somebody took rather than an accident the hardware supplied. That is this
machine — the clerk's column of pennies, restored.
Here is the problem this machine exists to solve. A computer stores ordinary
decimal numbers in floating point — binary fractions — and
binary simply cannot write down most decimal amounts exactly, in the same way
you can't write one-third as a finite decimal (0.3333… never ends). The value
$0.10 has no exact binary form; what the computer actually stores is a number a
hair's breadth away from a tenth. Each such amount is off by a sliver far too
small to see, and on its own it's harmless. But arithmetic piles those
slivers up. Add $0.10 to itself again and again, or run a few thousand
prices through a till, and the slivers accumulate until the total lands a whole
cent — sometimes more — off from what a person adding the same figures on paper
would get. The classic demonstration: on most computers 0.1 + 0.2
does not equal 0.3. It comes out as 0.30000000000000004.
For a physics simulation, nobody cares. For money, a total that's a penny wrong
is a bug report, an unbalanced ledger, an audit that doesn't reconcile.
The cure is to stop storing fractions at all. A dollar is a hundred cents,
and a cent is a whole thing — you never need to represent half a cent to a
customer. So money keeps the amount as a whole number of cents and
does all its sums on those whole numbers. Whole numbers add, subtract, and
compare with no drift whatsoever, because there are no fractions left to be
inexact about. Rounding still has to happen the moment you multiply by a rate
(8.75% tax on $19.99 genuinely lands between two cents). But here the rounding
is done once, on purpose, to the nearest cent, where you can
see it — not scattered invisibly through every operation. That's the whole idea:
exact where exactness is free, and honest, deliberate rounding only where the
arithmetic truly forces a choice. Reach for money any time real
money is involved — prices, totals, invoices, splitting a bill. And reach for
it instead of, never alongside, plain decimal dollars.
You make a Money from a number of dollars with
Dollars, or from a string a person typed with
MoneyVal. From there you add, subtract, sum a whole list,
compare, multiply by a rate, split into shares, and format for display. Because
every word is pure, you keep the result in a variable rather than expecting the
original to change.
Include "money.shoddy"
Def Main()
Let price = Dollars(19.99) ' 1999 cents, held exactly
Let tax = MoneyMul(price, 0.0875) ' 8.75% tax, rounded to the nearest cent
Let total = MoneyAdd(price, tax)
Print(MoneyFmt(price)) ' $19.99
Print(MoneyFmt(tax)) ' $1.75
Print(MoneyFmt(total)) ' $21.74
' Split a bill three ways with no penny lost:
Let shares = MoneySplit(Dollars(10.00), 3)
Each(shares, Fn(s) => Print(MoneyFmt(s))) ' $3.34, $3.33, $3.33
Print(MoneyLt(tax, price)) ' True
Print(MoneyFmt(MoneySum(shares))) ' $10.00 (the parts add back up)
A few things worth remembering:
Dollars and MoneyVal do that for you — and then stay
in Money for every sum after. Adding raw decimal numbers before
you wrap them just reintroduces the drift you came here to avoid.MoneyMul. Adding,
subtracting, and summing are exact and never round, because whole cents don't
need to. Multiplying by a rate is the only operation that can land between two
cents, and it rounds to the nearest one, explicitly. If you multiply, know that
a rounding happened there and nowhere else.MoneySplit
divides a sum into n shares as evenly as whole cents allow, then
hands the leftover pennies out one each to the earliest shares. So ten dollars
in three comes back as $3.34, $3.33, $3.33, which sums back to exactly $10.00.
No penny is created, none vanishes.MoneySub can go
below zero (a refund, a balance owed), and MoneyFmt formats a
negative amount with a leading minus, like -$4.50.Money is precise well past $90
trillion. You'll run out of realistic amounts long before you run out of
exactness, and no separate decimal type is needed.Every word, plus the Money type
| Word | Description |
|---|---|
| Money | An amount of money, held as a whole number of cents in its
Cents field. That's the entire representation — no fractions, no
hidden decimal. You rarely build one by hand; use Dollars or
MoneyVal. |
| Word | Description |
|---|---|
| Dollars(d) | Turns a number of dollars into a Money,
rounding to the nearest cent as it converts — Dollars(19.99) is
1999 cents. The usual way to start. |
| MoneyVal(s) | Turns a string a person typed (like
"19.99") into a Money, by reading it as a number of
dollars and then converting exactly as Dollars does. |
| Word | Description |
|---|---|
| MoneyAdd(a, b) | Adds two amounts. Exact — whole cents plus whole cents, no rounding, no drift. |
| MoneySub(a, b) | Subtracts b from a.
Exact, and may go negative (a refund or a balance owed). |
| MoneyMul(a, f) | Multiplies an amount by a plain number
f — a tax rate, an interest rate, a quantity — and rounds the
result to the nearest cent. This is the one word that rounds, and it does so
explicitly. |
| MoneySum(xs) | Adds up a whole list of Money values
into one total. Exact. An empty list sums to zero. |
| Word | Description |
|---|---|
| MoneyLt(a, b) | True or False: is a less than
b? (Since amounts are whole cents, equality is just plain
= on the values themselves.) |
| Word | Description |
|---|---|
| MoneySplit(m, n) | Divides m into n
shares as evenly as whole cents allow, handing the leftover pennies to the
earliest shares one apiece. The parts always sum back to exactly
m — no penny lost, none invented. |
| MoneyFmt(m) | Formats an amount as a familiar dollar string, like
$12.34 — always two decimal places, and thousands grouped with
commas: $1,234,567.89. Negative amounts get a leading minus,
like -$4.50. |
Sharing a program with clock?
Include both bare — they get along. (They once collided over twin private
Pad2 helpers. Both now lean on str's
PadZero instead, each bringing it in through its own
Include, and compiled include-once means one copy serves
everybody.)
| User | How | |
|---|---|---|
| cuttle | Money is the type CutMoney carries; MoneyAdd, MoneySub and MoneyFmt back its arithmetic and display. | |
| demographics | Dollars
and MoneyFmt print the predicted income as an exact dollar
figure beside the raw model output. |
| Machine | Why | |
|---|---|---|
| str | MoneyFmt
is CommaGroup dollars and PadZero cents. |