tfpdev
06 / 06Arithmetic

Money Math

Computers are bad at decimals: to them $0.10 + $0.20 is not $0.30. Split $100 three ways and a cent goes missing. Here is how to count money so it adds up.

01Step 1

Try it

Four small calculators. Each one is a mistake that looks fine on a single payment and costs real money over a month.

BreaksDecimals, the default way
what most software does unless told otherwise
0.30000000000000004a + b
0.30000000000000004a × 3
HoldsCount in cents
whole numbers only; the dot is added when a person looks
30 ¢ = $0.30a + b
30 ¢ = $0.30a × 3

Computers store 0.10 only approximately. One payment hides it; a month of them, added up and compared with the bank, does not. Count in whole cents and the problem disappears.

02Step 2

The three rules

Whole numbers, smallest coin

Store 4999, not 49.99, and always remember the currency. No decimals anywhere in the database.

Round once, on purpose

Where you round, which way, and who gets the leftover cent is a business decision. Write it down, do it in one place, store the result.

Add the dot at the very end

The number becomes '$49.99' only when a person is about to see it. Typing goes the other way: text straight to whole cents.

03Step 3

The code

The bad version does arithmetic on decimals and converts for the card company at the end. The good version counts in whole numbers from start to finish and adds the dot only for display.

Show the code anyway
BreaksBreaks
The version that loses money. Read the grey notes under it, not the code.
money-as-float.tsts
const price = 19.99;const total = price * qty;                  // 59.97000000000001const share = Math.round(total / 3 * 100) / 100;const minor = Math.round(total * 100);     // JPY: 100× too muchawait stripe.charges.create({ amount: minor });
Reads fine. Three of these four lines are wrong for at least one currency or one input.
HoldsHolds
The version running behind the buttons above.
money-as-integer.tsts
type Money = { minor: bigint; currency: string };export function split(total: bigint, parts: number): bigint[] {  const base = total / BigInt(parts);  let rem = total - base * BigInt(parts);  return Array.from({ length: parts }, () => base + (rem-- > 0n ? 1n : 0n));}export function minorDigits(currency: string): number {  return new Intl.NumberFormat("en", { style: "currency", currency })    .resolvedOptions().maximumFractionDigits;      // USD 2, JPY 0, KWD 3}// Postgres: amount_minor BIGINT NOT NULL, currency CHAR(3) NOT NULL// Never NUMERIC with implicit scale, never FLOAT, never "decimal in the ORM".
Integers in, integers through, formatting at the very edge. The two helpers below are the whole module.
04Step 4

Still curious?

A few more questions people ask. Open what you need.

Can't the database just store decimals exactly?

It can. The trouble is every other piece of software between the database and the screen, which turns decimals back into approximations. Whole numbers survive the trip.

Who gets the leftover cent?

Whoever the rule says. The rule matters less than having one and applying it the same way everywhere.

Is it ever fine to use decimals?

For a chart or a rough estimate, yes. For anything that will be added up, compared with the bank, or sent to the card company, no.

What does the card company want to receive?

Whole numbers in the smallest unit, plus the currency. If your own system counts the same way, there is nothing to convert and nothing to get wrong.