Skip to content
KEDBYTE
How Money Moves
Chapter
9

Currency, Decimals and the Money Type

Part I · What Money Is|8,418 words|about 37 min read|Volume 1

9.0 What this chapter gives you#

  1. You will be able to explain why ten 10p coins added together as pounds come to 0.9999999999999999, and why a till written to notice exactly £1.00 will never notice.
  2. You will be able to say why multiplying a yen amount by 100 charges the cardholder a hundred times too much, and why a Kuwaiti amount loses its last digit in a two-decimal system.
  3. You will be able to derive a currency’s minor unit from ISO 4217 rather than assuming 100, and name the exceptions: seventeen zero-decimal codes, seven three-decimal, two four-decimal and thirteen with no minor unit at all.
  4. You will be able to show that the 42-pence gap across 3,000 merchant fees came from an unwritten decision about where the rounding line falls, and not from a data type.
  5. You will be able to split a total across parts by the largest remainder method so that the parts always sum back to the total exactly.
  6. You will be able to say why bankers’ rounding is not “the correct rounding for money”, and cite the euro conversion rule and the VAT Notice 700 treatments that override it.
  7. You will be able to explain why a JSON number with a decimal point is the one wire convention that guarantees eventual trouble, and name the two that do not.
  8. You will be able to carry one internal representation — an integer count of minor units with an explicit currency — across ISO 8583, ISO 20022, SWIFT MT and Bacs Standard 18.
  9. You will be able to record a foreign exchange conversion so that an independent party can re-derive it, and say why a reference rate is not a transaction rate.
  10. You will be able to name the eight recurring failure modes of money handling and the single test that catches each one.

There is a category of bug that does not crash anything. The service stays up, the tests pass, and every individual transaction looks correct when you open it. Then, at the end of the month, the settlement report is three pence away from the bank statement and nobody can say why. Three pence is not a material amount of money. Explaining three pence to an auditor, and proving that it is not in fact three pence multiplied by every account you have not checked yet, can consume a fortnight.

This chapter is about the machinery that produces those three pence, and how to build a system in which they cannot occur. It is the most directly practical chapter in this volume for anyone who writes code, and it is also the chapter that finishes the volume’s argument. Chapter two established that a balance is a record rather than a substance. Chapter three established that every payment is two facts rather than one. This chapter establishes what kind of thing that record can be made of, and the answer is surprisingly constrained. Money is not a number. Money is a number, plus a currency, plus a rule about how finely that currency may be cut, plus a written decision about what happens when a calculation lands between two of the permitted cuts.

Leave out any one of those four and you have not built a ledger. You have built something that usually agrees with a ledger.

The plain version#

Think about a school tuck shop. Everything is priced in whole pennies: a chocolate bar at 65p, a packet of crisps at 48p, a drink at £1.20. At the end of the day the person running the shop tips the cash box onto the table and counts.

Nobody counts in pounds. They count in coins — four £1 coins, seven 50p pieces, twenty-three 20p pieces — and then convert everything into a single number of pennies, because pennies are the smallest thing the shop can actually receive. Nine hundred and forty-two pennies. Only at the very end, writing the figure on the sheet, does anybody turn 942 into “£9.42”.

That is the whole trick, and it is worth saying plainly because almost every serious money system in the world works this way. Count in the smallest coin. Convert to pounds only when you print something a human will read.

Why not just count in pounds?#

Because computers cannot hold “one tenth” exactly, and this is not a matter of them being sloppy. It is a matter of the ruler they use.

Imagine a ruler with no centimetre marks. Instead, someone has marked the halfway point, then the halfway points of each half, then the halfway points of those, and so on forever. You can mark exactly one half, one quarter, three eighths, eleven sixteenths. You can get extremely close to one tenth — closer than any eye could see. But you can never land exactly on it, because one tenth is not reachable by halving.

The ordinary fractional numbers a computer uses are built on exactly that ruler. And so when a computer stores the number 0.1, it does not store one tenth. It stores the nearest mark on the halving ruler, which is:

0.1000000000000000055511151231257827021181583404541015625

That is a real number, not an exaggeration for effect. It is the exact value your laptop puts in memory when you type 0.1. Now watch what happens. Ask a computer to add 0.1 and 0.2 and it produces:

0.3000000000000000444089209850062616169452667236328125

which it will politely display to you as 0.30000000000000004. Ask it whether 0.1 plus 0.2 equals 0.3, and it says no.

Try something even more homely. Put ten 10p coins into a till, one at a time, using pounds:

0.1, then 0.2, then 0.30000000000000004, then 0.4, then 0.5, then 0.6, then 0.7, then 0.7999999999999999, then 0.8999999999999999, and finally 0.9999999999999999.

Ten ten-pence coins have not made a pound. They have made a hair less than a pound. If the till software is written to notice when the drawer contains exactly £1.00, it will never notice.

Count in pennies instead and the problem does not arise. Ten lots of 10 is 100. Whole numbers on a computer are exact. There is nothing to go wrong.

Not every country cuts money the same way#

Here is the second idea, and it catches people out constantly because it is invisible from Britain. We assume every currency has a hundred small units in a big one. A pound has 100 pence, a dollar has 100 cents, a euro has 100 cents. So a programmer writes pence = pounds * 100 and moves on.

Japan does not work like that. The yen has no subdivision in use. A price of 2,500 yen is 2,500 yen; there is no such thing as 2,500.00. If you take a ¥2,500 payment and helpfully multiply by 100 before sending it, you have just asked for ¥250,000 — a hundred-fold error, and it will go through, because the number is perfectly valid.

Kuwait goes the other way. The Kuwaiti dinar is divided into 1,000 fils, so amounts carry three decimal places: 12.345 dinars is an ordinary price. Bahrain, Jordan, Oman, Iraq, Libya and Tunisia do the same. If your system assumes two decimal places, it will quietly shave the last digit off every Kuwaiti amount it touches.

So the rule is not “multiply by 100”. The rule is “look up how many places this particular currency has, and multiply by that”. There is an official list. It is called ISO 4217, and it gives every currency a three-letter code — GBP, USD, JPY, KWD — and tells you how many decimal places that currency uses.

A number on its own is not money#

Which brings us to the third idea. “£19.99” is money. “19.99” is not; it is a quantity of something unspecified. This sounds like pedantry and it is the single most useful discipline in the chapter. If a value in your system can be 19.99 without also carrying the letters GBP, then sooner or later somebody will add it to a euro amount, or send it to Japan, or store it in a column another team reads as dollars. The currency is not decoration on the amount. It is part of the amount, the way “miles” is part of “sixty miles”.

Splitting things up#

The last idea is the one that produces the three pence.

Three friends split a £5.00 bill. Five divided by three is £1.6666... Round each share to the nearest penny and each pays £1.67. Three times £1.67 is £5.01: they have paid a penny more than the bill.

Try it the other way. Three people split £100.00. A hundred divided by three is £33.3333... Round each to the nearest penny and each pays £33.33. Three times £33.33 is £99.99. A penny has vanished.

There is no clever arithmetic that avoids this. Five pounds genuinely does not divide into three equal whole numbers of pennies. What a well-built system does is not “avoid the problem” — it is decide, in advance and in writing, who gets the odd penny, and then make sure the parts always add back up to the whole. In the £5.00 case: two people pay £1.67 and one pays £1.66. In the £100.00 case: two pay £33.33 and one pays £33.34.

That is it. Count in the smallest coin. Look up how many small coins are in a big one for that particular currency. Never let an amount travel without its currency. And when you divide a total, force the pieces to add back up to it.

Where the plain version stops being true#

The plain version is the right instinct and it will keep a small system honest for years. Four things about it are wrong in ways that show up at scale, and the fourth is the one that costs money.

“Count in whole pennies” is only true at the settlement boundary#

The tuck-shop picture implies that every number in a financial system is a whole number of minor units. It is not, and insisting that it is will produce worse errors than it prevents.

Plenty of legitimate financial quantities are finer than a penny. Petrol in the United Kingdom is priced at, for example, 149.9 pence per litre. Interest accrues daily at rates like 4.37% per annum. Telecommunications and cloud services bill per second or per gigabyte at rates with four, six or eight decimal places. Foreign exchange rates carry six significant figures by law in the euro area. HM Revenue and Customs explicitly contemplates VAT calculated per article to three decimal places of a pound.

The correct statement is narrower and more useful. The amount that settles must be a whole number of minor units. Intermediate quantities need not be, and the design question is where the rounding line falls. A rate is not money. A price per unit is not money. A running accrual is not money until it is capitalised. Money is what leaves one account and arrives in another, and that has to be a whole number of the smallest thing the payment system can carry.

A system that rounds too early loses accuracy for no reason. A system that rounds too late presents a settlement instruction that no rail will accept. Both failures come from not writing down where the line is.

Floating point is not “imprecise” — it is exact in the wrong base, and that is worse#

The plain version says computers cannot hold one tenth. True, but the framing invites the wrong conclusion: that the errors are random, tiny, and can be mopped up by rounding at the end.

First, binary floating point is not approximate. Every value it holds is exactly a whole number multiplied by a power of two, and the arithmetic is exactly specified and perfectly repeatable. The problem is that the set of values it can hold does not include the numbers a ledger is made of, so a calculation that ought to be exact acquires a small, deterministic, unpredictable-by-eye residue. That is worse than randomness, because it survives testing. Every one of your test cases will pass and the one combination you did not try will be off by a hair.

Second, and more practically: the damage almost never arrives as a visibly wrong total. It arrives in three other ways.

It arrives as a failed equality test. Debits of £1,234.56, £78.90, £4,321.00, £0.07 and £999.99 against a single credit of £6,634.52 plainly balance. Add the debits in binary floating point and you get 6634.5199999999995, so total_debits == total_credits returns false. Your double-entry validator, the one from chapter three that is supposed to be the last line of defence, now rejects a correct journal — or, if somebody has “fixed” it with a tolerance, now accepts an incorrect one.

It arrives as rounding that goes the wrong way. Ask a computer to round 1.15 to one decimal place and it returns 1.1, not 1.2 — not because the rounding rule is wrong but because the value actually stored for 1.15 is 1.14999999999999991118..., genuinely below the midpoint. The function did exactly what it was asked. The input was never what you thought.

And it arrives at serialisation boundaries, because JSON has no decimal type and a JSON number is in practice parsed into a binary double. An amount that was exact in your database becomes inexact the moment it crosses an API.

And here is the part that reframes the whole subject. In a run of 3,000 sales at £19.99, the floating-point error in the total is about 1.6 billionths of a penny — real, but not what will cost you money. Now apply a merchant fee of 1.4% plus 20p to each of those sales. If you round the fee per transaction, each is 27.986p plus 20p, rounded to 48p, and the total is £1,440.00. If you compute the fee on the whole run, it is £1,439.58. The gap is 42 pence, and floating point had nothing to do with it. It came entirely from an unwritten decision about where the rounding line falls. That is the shape of nearly every real money bug: a rounding policy nobody specified, not a data type nobody understood.

Minor units are a convention, and the convention is not universally agreed#

The plain version implies that “how many decimal places does this currency have” is a fact you can look up once. It is a convention recorded by a standards body, and there are at least three ways it drifts from reality.

First, ISO 4217 itself rounds off awkward cases. The Malagasy ariary is notionally divided into five iraimbilanja and the Mauritanian ouguiya into five khoums — the only two circulating currencies whose subdivision is not a power of ten. ISO 4217 assigns both a minor unit of 2 anyway, because a field that holds an exponent has nowhere to record “one fifth”. The standard is doing something sensible; it is simply not describing the currency.

Second, processors disagree with the standard, and with each other, for backwards-compatibility reasons. Stripe’s published list of zero-decimal currencies includes MGA, which ISO 4217 gives a minor unit of 2. In the other direction, ISO 4217 gives both the Icelandic króna and the Ugandan shilling a minor unit of 0, but Stripe’s documentation states that both “transitioned to a zero-decimal currency” while “backward compatibility requires you to represent it as a two-decimal value, where the decimal amount is always 00” — so to charge 5 ISK you submit 500. Stripe further treats the Hungarian forint and the New Taiwan dollar as zero-decimal for payouts while accepting two-decimal charges in them, requiring payout amounts to be evenly divisible by 100. None of this is wrong. All of it means “the minor unit” is a property of a currency within a particular scheme’s rules, not of the currency alone.

Third, the smallest unit you can account in and the smallest unit you can pay in are different things. Several countries have withdrawn their smallest coin and round cash transactions to the nearest five units at the till while still recording electronic payments to the single unit. A ledger that models one granularity cannot represent the gap between the invoiced amount and the cash tendered.

The practical consequence: derive the exponent from a table you control and can version, validate it against the scheme you are actually talking to, and never from a hard-coded 100 or from whatever your standard library’s locale data says this month.

Bankers’ rounding is not “the correct rounding for money”#

“Use bankers’ rounding for money” circulates as settled wisdom. It is not settled, and applying it where the law says otherwise is a compliance problem rather than a style preference.

Round-half-to-even exists for a good reason: rounding halves consistently upwards introduces a systematic upward bias. Take the ten values 0.005, 0.015, 0.025 and so on up to 0.095. Their true sum is exactly 0.50. Round each half upwards and they sum to 0.55; round each to the nearest even last digit and they sum to 0.50. Over a large book of similar figures the half-up rule drifts and the half-even rule does not. That is a genuine and important property.

But it is a statistical argument, and statistics do not override statute. Council Regulation (EC) No 1103/97, which still governs conversion between the euro and the legacy national currencies it replaced, says in Article 5: “If the application of the conversion rate gives a result which is exactly half-way, the sum shall be rounded up.” Not to even. Up. In the United Kingdom, VAT Notice 700 permits an invoice trader to round the total VAT on an invoice down to a whole penny — an explicit concession in the taxpayer’s favour — while stating that the same concession “is not appropriate to retailers”, who “must not round the VAT figure down”.

So there is no single rounding mode for money. There are modes mandated by specific regimes for specific calculations, and the job is to know which regime you are in. A system that applies half-even everywhere because a blog post recommended it will produce VAT figures HMRC’s own guidance does not describe.

The second half of this correction is the one that generates reconciliation breaks. Rounding each component and then adding is not the same as adding and then rounding. Three items at £8.99 with VAT at 20%: each line’s VAT is £1.798, which rounds to £1.80, so the invoice total from line rounding is £5.40. Compute VAT on the £26.97 order total and you get £5.394, which rounds to £5.39. Both figures are defensible. They are one penny apart. If your invoice engine uses one method and your settlement engine the other, you will be reconciling that penny for the life of the product.

The technical version#

ISO 4217: the code, the number and the exponent#

ISO 4217 is the international standard for representing currencies and funds; the current edition is ISO 4217:2015. The maintenance agency is SIX Financial Information AG, acting on behalf of the Swiss Association for Standardization, which publishes the authoritative tables and issues amendments as currencies are introduced, redenominated or withdrawn.

The standard defines three tables. List One contains the currently active currency and funds codes. List Two contains fund codes. List Three is the historical list of withdrawn codes — a list that exists precisely because currency codes are not permanent, and a system that stores only a code, with no effective date, cannot always reconstruct what an old record meant.

Each entry in List One carries the country or entity, the currency name, a three-character alphabetic code, a three-digit numeric code, and the minor unit — the number of decimal places conventionally used. The alphabetic code is normally the two-letter ISO 3166-1 country code followed by the initial of the currency name: GB plus P gives GBP, US plus D gives USD, JP plus Y gives JPY. Supranational and non-country codes begin with X, which ISO 3166-1 reserves for exactly this purpose. That is why gold is XAU, silver XAG, platinum XPT and palladium XPD; why the IMF’s Special Drawing Right is XDR; why the CFA francs are XOF and XAF; and why XTS is reserved for testing and XXX denotes “no currency involved”. The three-digit numeric code exists because a great deal of financial messaging is numeric-only: GBP is 826, USD is 840, EUR is 978, JPY is 392, INR is 356, KWD is 414.

As published on 1 January 2026, List One contains 178 distinct alphabetic codes spread over 280 country-and-currency rows. The overwhelming majority — 139 codes — have a minor unit of 2. The exceptions are the ones your code has to handle.

Minor unit Count Codes (with numeric code)
0 17 BIF 108, CLP 152, DJF 262, GNF 324, ISK 352, JPY 392, KMF 174, KRW 410, PYG 600, RWF 646, UGX 800, UYI 940, VND 704, VUV 548, XAF 950, XOF 952, XPF 953
2 139 GBP 826, USD 840, EUR 978, INR 356 and 135 others
3 7 BHD 048, IQD 368, JOD 400, KWD 414, LYD 434, OMR 512, TND 788
4 2 CLF 990 (Unidad de Fomento), UYW 927 (Unidad Previsional)
N.A. 13 XAG 961, XAU 959, XBA 955, XBB 956, XBC 957, XBD 958, XDR 960, XPD 964, XPT 962, XSU 994, XTS 963, XUA 965, XXX 999

Three observations. The four-decimal entries are not ordinary currencies: CLF, Chile’s Unidad de Fomento, and UYW, Uruguay’s Unidad Previsional, are inflation-indexed units of account, and they appear in any complete currency table you load, so a validator assuming nothing exceeds three decimals will reject them. The thirteen “N.A.” entries have no minor unit at all — XAU is not a currency but a troy ounce of gold, and if your money type looks up its exponent and defaults to 2 on a miss, you have silently invented centigrams of gold. And XXX exists: it is the correct code for a transaction where no currency is involved. It is not a null and it is not an error.

Representing an amount: the money type#

A money value is a triple: an integral quantity, a currency, and the exponent that relates them. Systems usually store the first two and derive the third, which is defensible provided the currency table is versioned and the derivation happens once, at the edge.

The canonical representation is an integer count of minor units. GBP 19.99 is stored as 1999 with currency GBP; JPY 2500 as 2500 with currency JPY; KWD 12.345 as 12345 with currency KWD. There is no decimal point anywhere in storage. Formatting for display happens at the last possible moment, and parsing user input at the first.

The range question is usually settled quickly. A signed 64-bit integer holds up to 9,223,372,036,854,775,807, which in pence is £92,233,720,368,547,758.07 — comfortably more than the money supply of any country. It is not unlimited: currencies with large nominal values at institutional scale, hyperinflationary histories, and any field that also stores intermediate accruals at higher precision will get there sooner than you expect.

Where an integer is not enough, use a decimal type — a numeric type whose radix is ten, so that one tenth is exactly representable.

Type Representation Practical limit
java.math.BigDecimal Arbitrary-precision unscaled BigInteger with a 32-bit scale Effectively unbounded; scale is per-value and preserved
decimal.Decimal (Python) Arbitrary-precision, context-controlled Default context precision of 28 significant digits, adjustable
System.Decimal (.NET) 128 bits: a 96-bit integer plus sign and a scaling factor of 10 to the power 0 through 28 Maximum 79,228,162,514,264,337,593,543,950,335
IEEE 754 decimal32 / decimal64 / decimal128 Decimal floating point 7, 16 and 34 significant decimal digits respectively
SQL NUMERIC / DECIMAL Implementation-defined exact numeric PostgreSQL: up to 131,072 digits before and 16,383 digits after the decimal point

The PostgreSQL documentation is unusually direct about which of its numeric types to use: numeric “is especially recommended for storing monetary amounts and other quantities where exactness is required”, while real and double precision “are inexact, variable-precision numeric types” where “some values cannot be converted exactly to the internal format and are stored as approximations, so that storing and retrieving a value might show slight discrepancies.”

Two traps around that. PostgreSQL also ships a type literally called money, whose fractional precision comes from the database’s lc_monetary setting, so its behaviour depends on server configuration and it carries no currency identifier. And an object-relational mapper that maps a NUMERIC column to a language-level double undoes the benefit of the column type without producing any error at all. Check the mapping, not the schema.

The serialisation boundary deserves the same suspicion. JSON has no decimal type; JSON numbers are commonly parsed into IEEE 754 binary64, and in JavaScript integers above Number.MAX_SAFE_INTEGER, which is 9,007,199,254,740,991, lose exactness. The two safe conventions are to transmit an integer count of minor units alongside a currency code, as most card-processing APIs do, or to transmit the amount as a JSON string and parse it into a decimal type, as many banking APIs do. A JSON number with a decimal point is the one option that guarantees eventual trouble.

Why binary floating point cannot carry money#

IEEE 754 binary64 — double in C, Java and JavaScript, float in Python — is 64 bits: one sign bit, an 11-bit exponent, and 52 stored significand bits giving 53 bits of effective precision. Every finite value it represents is of the form m times 2 to the power e, with m and e integers, so the representable numbers are exactly the dyadic rationals within range: fractions whose denominator is a power of two.

One tenth is not a dyadic rational. Neither is one hundredth. The two quantities a currency system is built out of are precisely the quantities binary floating point cannot hold. The nearest binary64 value to 0.1 is

0.1000000000000000055511151231257827021181583404541015625

and the nearest to 0.2 is

0.200000000000000011102230246251565404236316680908203125

Their exact sum, correctly rounded to the nearest binary64 value, is

0.3000000000000000444089209850062616169452667236328125

whereas the nearest binary64 value to the literal 0.3 is

0.299999999999999988897769753748434595763683319091796875

These are different numbers. The arithmetic is not wrong; it is exactly right about a question you did not intend to ask.

The consequences in a ledger are specific and testable:

Equality fails. Adding the debits £1,234.56, £78.90, £4,321.00, £0.07 and £999.99 in binary64 yields 6634.5199999999995 against a credit of 6634.52 — a residue of about minus 9.1 times 10 to the minus 13. Double-entry validation by exact comparison rejects a correct journal.

Accumulation drifts. Adding 0.01 one million times gives 10000.000000171856. Adding 0.07 one thousand times gives 69.99999999999966.

Rounding lands on the wrong side. round(1.15, 1) gives 1.1 and round(2.675, 2) gives 2.67, because the stored values are 1.14999999999999991118... and 2.67499999999999982236..., both genuinely below the midpoint. The nearest-even rule is not the culprit; the input never was a midpoint.

Truncation compounds. The Vancouver Stock Exchange index is the standard historical illustration, and because it is a truncation error rather than a binary-representation error it is the more general lesson. The index was established at 1,000.000 in January 1982 and recalculated on every trade, with each updated value truncated rather than rounded to three decimal places. Losing a fraction of a thousandth on each of thousands of daily recalculations, it drifted downwards relentlessly, and by the close on Friday 25 November 1983 it stood at 524.811. Over the weekend of 25 to 28 November the calculation was corrected and the index restated at 1,098.892 — more than double the published figure. Nothing had happened in the market. The index had simply been throwing away a sliver, several thousand times a day, for twenty-two months.

The deeper argument against floating point is not accuracy at all. A ledger has to be reproducible by an independent party. Your merchant, your bank, your auditor and your regulator must be able to take the same inputs and the same stated rules and arrive at your figure, digit for digit, using different software. Exact decimal arithmetic on integer minor units makes that trivial. Binary floating point makes it a research project.

Rounding: modes, mandates, and the invariant#

The named rounding modes a practitioner needs are few.

Mode Behaviour at a tie Also called
Round half up Away from zero Round half away from zero, commercial rounding, arithmetic rounding
Round half down Towards zero
Round half to even To the nearest even last digit Bankers’ rounding; IEEE roundTiesToEven
Round towards zero Not applicable; always truncates Truncation, chopping; IEEE roundTowardZero
Round towards positive infinity Always upward Ceiling; IEEE roundTowardPositive
Round towards negative infinity Always downward Floor; IEEE roundTowardNegative

IEEE 754 specifies roundTiesToEven as the default attribute for binary formats and requires roundTiesToAway — round half away from zero — to be available for decimal formats, which is the standard acknowledging that commercial practice does not universally use ties-to-even.

The bias argument for ties-to-even is worth being able to demonstrate. The ten values 0.005, 0.015, 0.025, 0.035, 0.045, 0.055, 0.065, 0.075, 0.085 and 0.095 sum to exactly 0.500. Rounded half up to two places they become 0.01 through 0.10 and sum to 0.55, an overstatement of ten per cent of the total. Rounded half to even they become 0.00, 0.02, 0.02, 0.04, 0.04, 0.06, 0.06, 0.08, 0.08, 0.10 and sum to exactly 0.50.

Now the mandates, which override the argument.

Euro conversion. Council Regulation (EC) No 1103/97 fixes the arithmetic in law. Conversion rates “shall be adopted as one euro expressed in terms of each of the national currencies” and “shall be adopted with six significant figures”. They “shall not be rounded or truncated when making conversions”. “Inverse rates derived from the conversion rates shall not be used” — you may not divide by the published rate to go the other way. Amounts moving between two national currency units “shall first be converted into a monetary amount expressed in the euro unit, which amount may be rounded to not less than three decimals and shall then be converted into the other national currency unit” — mandatory triangulation through the euro. And on ties, Article 5: “If the application of the conversion rate gives a result which is exactly half-way, the sum shall be rounded up.”

United Kingdom VAT. VAT Notice 700 sets out the permitted treatments. At paragraph 17.5, “You may round down the total VAT payable on all goods and services shown on a VAT invoice to a whole penny”, with the caveat that this “concession to round down amounts of VAT is designed for invoice traders and applies only where the VAT charged to customers and the VAT paid to HMRC is the same.” At 17.5.1, where VAT is worked out per line, the trader must round “down to the nearest 0.1 pence” or “to the nearest 1 pence or 0.5 pence”, and “Whatever you decide, you must be consistent.” At 17.5.2, VAT per unit or per article must be worked to “4 digits after the decimal point and then round to 3 digits” or to the nearest penny or half penny, with an explicit prohibition on rounding down to nil on any unit liable at the standard or reduced rate. At 17.6, retailers who calculate VAT at line or invoice level “must not round the VAT figure down”, though they “may round (up and down) each VAT calculation”.

Read those two regimes together and the design conclusion is inescapable: the rounding mode is a per-calculation configuration item with a legal basis, not a global constant.

The invariant that must hold regardless of mode is this. For any total that is broken into parts, the parts must sum exactly to the total, in integer minor units, with no residual. Enforce it as an assertion, not as a hope.

Allocation: dividing a total without creating or destroying money#

Naive per-part rounding violates that invariant in both directions. Splitting GBP 5.00 three ways gives 166.666... minor units per part; rounding each gives 167, and three parts of 167 total 501 — a penny created from nothing. Splitting GBP 100.00 three ways gives 3,333.33... per part; rounding each gives 3,333, totalling 9,999 — a penny destroyed.

The standard fix is the largest remainder method, borrowed from apportionment in electoral mathematics. Compute each part’s exact share as a rational number of minor units, take the floor of each, and subtract the sum of the floors from the total to get the residual — always a non-negative integer strictly less than the number of parts. Distribute that residual one minor unit at a time to the parts with the largest fractional remainders, breaking ties by a deterministic rule, such as line sequence, recorded in the specification.

Worked, with a GBP 10.00 order-level discount to be apportioned across three lines of GBP 3.99, GBP 12.50 and GBP 7.51:

Line Line total (minor units) Exact share of 1000 Floor Fractional remainder Final
A 399 166.2500 166 0.2500 166
B 1250 520.8333 520 0.8333 521
C 751 312.9167 312 0.9167 313
Total 2400 1000.0000 998 residual 2 1000

The floors total 998, leaving a residual of two minor units, which go to lines C and B in descending order of remainder. The allocation is GBP 1.66, GBP 5.21 and GBP 3.13, summing exactly to the GBP 10.00 discount.

The alternative is the running-remainder method: process the parts in a fixed order, and for each allocate the difference between the rounded cumulative target and the cumulative amount already allocated. It also guarantees the invariant and is order-dependent by construction, which is fine as long as the order is deterministic and documented.

Whichever you choose, the properties to test are the same. The sum of the parts equals the total exactly. Every part carries the same sign as the total, so an allocation of a positive amount never emits a negative part. The function is deterministic. And the result is stable under reversal: allocating a refund of the same total across the same parts must produce the same pieces, or your refund will not net your original to zero.

The places this bites in production are consistent across the industry: marketplace payouts split among sellers, order-level discounts and shipping apportioned across lines for VAT, subscription proration on mid-cycle upgrades, interchange and scheme fees attributed to individual transactions from a netted invoice, and partial refunds allocated back across multiple tenders.

Multi-currency, in outline#

Two rules first, because they eliminate most multi-currency defects before they exist. An amount never exists without its currency, enforced in the type system where the language permits it. Two amounts in different currencies are not addable, and an attempt should be an error rather than a number.

A ledger account is denominated in exactly one currency. A multi-currency ledger is therefore not a ledger of mixed rows; it is a set of single-currency ledgers, plus explicit foreign exchange gain and loss accounts to absorb the difference when a position is revalued. IAS 21, The Effects of Changes in Foreign Exchange Rates, requires foreign currency monetary items to be translated at the closing rate at each reporting date, with the resulting exchange differences recognised in profit or loss in the period in which they arise. That is what those accounts exist to satisfy.

Any conversion must be recorded with five things, or it cannot be re-derived: source amount and currency, target amount and currency, the rate as an exact decimal, the quotation direction, and the timestamp with the identity of the rate source. A stored rate of “1.1642” with no direction is not information. Market convention writes a pair as base and quote — EUR/USD 1.0850 means one euro buys 1.0850 dollars — but conventions are not universal across systems, so store the direction rather than infer it.

Reference rates are not transaction rates, and the institutions that publish them say so plainly. The European Central Bank’s euro foreign exchange reference rates are, in the ECB’s own words, “usually updated at around 16:00 CET every working day, except on TARGET closing days”, based on “the daily concertation procedure between central banks across Europe, which normally takes place around 14:10 CET”. The ECB adds that they “are published for information purposes only” and that “Using the rates for transaction purposes is strongly discouraged.” A rate you can deal on comes from a counterparty with a bid and an offer around it, and the mid-point between the two is not a price anyone will give you.

Rounding at conversion follows the target currency, not the source: converting into JPY must produce a whole number of yen, converting into KWD a multiple of one fils. Converting an amount, rounding it, and converting it back will not return the original, and any process that assumes it does — a refund path, a reversal, a reconciliation — is broken. So is chained conversion: A to B to C does not in general equal A to C, which is exactly why Regulation 1103/97 mandates a specific triangulation route with a specified intermediate precision rather than leaving it to the implementer.

How the amount travels on the wire#

The four message families a UK payments engineer meets carry amounts four different ways, and the differences are instructive.

ISO 8583, the card authorisation format taken apart in Volume III, carries the amount in data element 4, Amount, Transaction, a fixed-length twelve-digit numeric field. There is no decimal point and no exponent in the field itself; the position of the decimal is implied entirely by data element 49, Currency Code, Transaction, which carries the three-digit ISO 4217 numeric code. A charge of GBP 19.99 travels as 000000001999 with DE49 set to 826. A charge of JPY 2,500 travels as 000000002500 with DE49 set to 392 — and if the terminal has multiplied by 100 on the way in, the message is still perfectly well-formed and the cardholder is charged a hundred times too much. Nothing in the format can detect that error.

ISO 20022, the XML-based successor, carries the amount as a decimal with the currency as an attribute on the element: an instructed-amount element whose Ccy attribute is GBP and whose text content is 19.99. The schema type restricts the value to at most 18 total digits and 5 fractional digits, with a minimum of zero. That schema is deliberately permissive — 5 fractional digits exceeds any List One currency — and the real constraint is a business rule stated in the Message Definition Reports: “The number of fractional digits (or minor unit of currency) must comply with ISO 4217.” A validator accepts 10.21, 10.2 and 10 for EUR and rejects 10.403. Schema validation alone is not sufficient validation.

SWIFT MT messages carry the amount in fields such as 32A, Value Date, Currency Code, Amount, with the format 6!n3!a15d — six numeric characters of date in YYMMDD, three alphabetic characters of currency code, and an amount of up to fifteen characters. The network validated rules are explicit: “The integer part of Amount must contain at least one digit. The decimal comma ‘,’ is mandatory and is included in the maximum length. The number of digits following the comma must not exceed the maximum number allowed for that specific currency as specified in ISO 4217.” The decimal separator is a comma, it is mandatory even for a whole amount — five hundred thousand is written 500000, — and it consumes one of the fifteen characters.

Bacs Standard 18, the fixed-length format carrying UK Direct Debit and Direct Credit submissions, takes the strictest line of the four. The amount occupies eleven characters at positions 36 to 46 of the record and is expressed in pence: all numeric, not all zeros, right-justified and zero-filled. There is no currency field, because the service is sterling only, and no decimal point anywhere in the file. A payment of £19.99 is written 00000001999.

One internal representation; four adapters. Hold the amount as an integer count of minor units with an explicit currency, convert at each boundary, and look the exponent up from a versioned table rather than assuming it.

The failure modes, and the test that catches each#

Failure mode Symptom Test
Binary floating point in a ledger field Reconciliation off by fractions of a penny; balance checks fail sporadically Assert exact equality of debits and credits with no tolerance
Hard-coded exponent of 2 Yen charged 100 times too much; fils truncated on Kuwaiti amounts Round-trip a JPY, a KWD and a CLF amount through every boundary
Naive per-part rounding Allocated parts sum to one minor unit more or less than the total Property test: for random totals and weights, sum of parts equals total
Unspecified rounding line Two subsystems disagree by a penny per transaction Assert invoice total equals the sum of line totals
Amount without currency Cross-currency addition produces a plausible wrong number Make currency a required field; forbid addition of unlike currencies
Conversion without stored rate Historical figures cannot be re-derived Persist source, target, rate, direction, timestamp and rate source
JSON number for an amount Value changes across an API round trip Transmit integer minor units or strings; assert identical round trip
Currency code without effective date Redenominated historical records misread Store the effective date; retain List Three codes rather than remapping

The point of all this#

This chapter belongs in a volume called What Money Is, rather than in an appendix on software engineering, because the money type is where the volume’s argument becomes physical.

If money were a substance, its representation would be a detail. You would weigh it, and the scales would either be accurate or not. But money is a record — a claim, recorded twice, denominated in a unit some authority defines. And a record has to be exact, not merely accurate, because its whole function is that two parties who do not trust each other can compute the same answer from it and be bound by the result. A ledger uses integer minor units and a documented rounding rule not out of fastidiousness, but because the alternative — a number that is very nearly right, arrived at by a process the counterparty cannot reproduce — is not a record of anything.

Three pence is not a material amount of money. But three pence you cannot explain is a defect in the only property a ledger has.

9.98 Common wrong ideas#

Wrong: Every number in a financial system must be a whole number of minor units. Right: Only the amount that settles must be; rates, per-unit prices and running accruals are legitimately finer, and the design question is where the rounding line falls.

Wrong: Floating point is merely imprecise, so the small random errors can be mopped up by rounding at the end. Right: It is exact in the wrong base, so the residue is deterministic and survives testing — every case you thought to write passes, and the one combination you did not try is off by a hair.

Wrong: The damage from floating point shows up as a visibly wrong total. Right: It shows up as a failed equality test in your double-entry validator, as rounding that lands on the wrong side, and at serialisation boundaries where JSON has no decimal type.

Wrong: round(1.15, 1) returning 1.1 is a fault in the rounding rule. Right: The value actually stored is 1.14999999999999991118..., genuinely below the midpoint; the function did what it was asked and the input was never what you thought.

Wrong: Money bugs come from choosing the wrong data type. Right: The 42-pence gap on a run of 3,000 sales came entirely from computing the merchant fee per transaction rather than per run, which is a rounding policy nobody specified.

Wrong: How many decimal places a currency has is a fact you can look up once. Right: ISO 4217 rounds off the ariary and the ouguiya, processors disagree with the standard and with each other, and the smallest unit you can pay in is not always the smallest you can account in.

Wrong: Bankers’ rounding is the correct rounding for money. Right: Council Regulation (EC) No 1103/97 requires an exact half to be rounded up, and VAT Notice 700 lets an invoice trader round total VAT down while forbidding retailers from doing so.

Wrong: Rounding each line and then adding gives the same answer as adding and then rounding. Right: Three items at £8.99 with VAT at 20% give £5.40 by line and £5.39 by order total, and if your invoice engine and your settlement engine disagree you will reconcile that penny for the life of the product.

Wrong: Rounding each share to the nearest penny divides a total fairly. Right: £5.00 three ways creates a penny from nothing and £100.00 three ways destroys one, so the parts must be forced to sum back to the whole.

Wrong: A currency whose exponent is missing can safely default to 2. Right: XAU has no minor unit at all, so defaulting it silently invents centigrams of gold; and XXX, meaning no currency involved, is neither a null nor an error.

9.99 Chapter summary in 20 lines#

  1. Money is not a number: it is a number, plus a currency, plus a rule about how finely that currency may be cut, plus a written decision about what happens between two permitted cuts.
  2. The working discipline is to count in the smallest coin and convert to pounds only when printing something a human will read.
  3. Binary floating point cannot hold one tenth or one hundredth, because every value it represents is a whole number multiplied by a power of two.
  4. Ten ten-pence coins added as pounds make 0.9999999999999999, so a till checking for exactly £1.00 never fires.
  5. The arithmetic is not approximate but exact in the wrong base, which is worse, because a deterministic residue survives every test you thought to write.
  6. The damage arrives as failed equality tests, as rounding that lands on the wrong side, and at API boundaries where an exact amount is parsed into a binary double.
  7. Not every currency has a hundred minor units: the yen has none in use and the Kuwaiti dinar has a thousand fils, so the exponent must be looked up rather than assumed.
  8. ISO 4217 gives every currency a three-letter code, a three-digit numeric code and a minor unit, and its List Three of withdrawn codes exists because codes are not permanent.
  9. Of 178 active alphabetic codes, 139 have a minor unit of two, seventeen have none, seven have three, two have four, and thirteen have no minor unit at all.
  10. The minor unit is a property of a currency within a particular scheme’s rules rather than of the currency alone, so derive it from a table you control and version.
  11. The canonical representation is an integer count of minor units with an explicit currency, and there is no decimal point anywhere in storage.
  12. Where an integer will not stretch, use a decimal type whose radix is ten, and check that no object-relational mapper quietly turns the column back into a double.
  13. The named rounding modes are few, and the bias argument for ties-to-even is genuine, but statistics do not override statute.
  14. Euro conversion is fixed in law: six significant figures, no inverse rates, mandatory triangulation through the euro, and exact halves rounded up.
  15. United Kingdom VAT permits an invoice trader to round total VAT down and forbids a retailer from doing so, which makes the rounding mode a per-calculation configuration item with a legal basis.
  16. Rounding each component and then adding is not the same as adding and then rounding, and one penny of disagreement between two subsystems is permanent.
  17. Whatever the mode, the parts of any divided total must sum exactly to the total in integer minor units, enforced as an assertion rather than hoped for.
  18. The largest remainder method achieves that: take the floor of each exact share, then hand the residual out one minor unit at a time to the largest fractional remainders, with a deterministic tie-break.
  19. An amount never travels without its currency, an account is denominated in exactly one currency, every conversion records source, target, rate, direction, timestamp and rate source, and a published reference rate is not a rate you can deal on.
  20. A ledger must be exact rather than merely accurate, because its whole function is that two parties who do not trust each other can compute the same answer from it — and three pence you cannot explain is a defect in the only property a ledger has.

Sources used: SIX Financial Information — ISO 4217 currency code List One, published 1 January 2026; ISO — ISO 4217 and ISO 4217:2015. EUR-Lex — Council Regulation (EC) No 1103/97, Articles 4 and 5. HM Revenue and Customs — VAT Notice 700, paragraphs 17.5, 17.5.1, 17.5.2 and 17.6. European Central Bank — Euro foreign exchange reference rates. IEEE 754 Standard for Floating-Point Arithmetic. PostgreSQL documentation, section 8.1 Numeric Types. Microsoft .NET API documentation, System.Decimal. Stripe — Supported currencies. ISO 20022 — ActiveCurrencyAndAmount schema restriction and Message Definition Report rule on fractional digits, with XMLdation’s validation notes. SWIFT field 32A network validated rules, via IBM Financial Transaction Manager documentation. Bacs Standard 18 record layout as reproduced in submitter documentation. The Vancouver Stock Exchange restatement of 25 to 28 November 1983, reported in The Wall Street Journal and catalogued in the numerical-analysis collections at TU Delft and the University of Texas at Austin. Floating-point values here were computed directly in IEEE 754 binary64 and Python’s decimal module.