TT Lab
Get started
Learn Learning paths Courses

The Language of Banking

The Totals Are Off by One Won and No One Can Reproduce It

Continue in TT Lab

In one line

The number of decimal places differs by currency, and if you leave those places to floating point, the total quietly drifts from the ledger. The whole of this problem is pinning down storage as integer minor units, the rounding position as policy, and the remainder as an allocation rule.

Why this was needed

A fee settlement batch starts hearing, from some day on, that "the total is off by 3 won." When the person in charge opens the table and checks line by line, no line is wrong. But when you add them up, it differs. Asked to reproduce it, they feed the same input again and then it matches. After this goes on for about two months, an operating procedure arises: "if you rerun the batch, it matches."

The cause is usually in two places. Amounts were held as double, or the rounding position was decided differently in each piece of code. Both are off by no more than 1 per line, so unit tests cannot catch them. Test data uses nice numbers like 12.34, and on nice numbers nothing goes wrong.

When currencies are mixed in, one more problem attaches. "Two decimal places" is the dollar's circumstance, not a rule of the world. The won and the yen have 0 decimal places, and the Kuwaiti dinar and the Bahraini dinar have 3. Codes for which the very concept of decimal places does not exist, such as gold (XAU), are in the same table too.

How it works

The maintenance agency for currency codes is SIX, and the source of the list is List One XML. The CcyMnrUnts of each entry is the number of decimal places of that currency. These are values read directly from the edition issued on 2026-01-01.

KRW 0   JPY 0   CLP 0        (최소단위가 통화 단위 그 자체)
USD 2                        (센트)
KWD 3   BHD 3   TND 3        (필스)
XAU N.A.                     (금. 소수 자리가 숫자가 아니다)

So the first decision for code that handles amounts is to store in integer minor units. 1,234.56 dollars is stored as two values, 123456 and USD, and the decimal point is inserted only when displaying. That way there is no place for error to arise in addition and subtraction.

The only places where error can arise are multiplication and division. When you multiply by a rate, digits smaller than the minor unit appear, and you have to decide how to cut those digits. Python's decimal module cuts digits with quantize() and lets you choose modes such as ROUND_HALF_UP and ROUND_HALF_EVEN. The default is banker's rounding (ROUND_HALF_EVEN), and Python's built-in round() is the same. So code written in the belief that "0.5 rounds up" gives a different answer in exactly half of the cases. Which of the two is right is not decided by technology. The answer is to write down in a document which one it is and make the code use only that one.

The same pitfall exists on the storage side. In SQLite's data types, types attach to values, not columns, and if you put a real number in a cell declared INTEGER, it goes in as REAL. The PostgreSQL numeric documentation states clearly that numeric does exact computation but is slow while double precision is the opposite, and then advises using the exact one for values that handle money.

The last piece is allocation. If you split 1,000 won among 3 people, it is 333, 333, 333 with 1 won left over. If you throw the 1 won away, the total does not match, and if you round it up for everyone, you exceed the total. The largest remainder method gives the quotients rounded down and then gives 1 each to the ones with the largest remainders for as much as is left. That way the total matches exactly, and who received the extra 1 won is also explained by the rule.

What it looks like in the field

First, people fight over the conversion order. The value obtained by converting line by line and rounding to the won unit and then adding differs from the value obtained by adding everything first and converting once. Neither is a wrong calculation; they are different contracts. If each line on the invoice must show won, it is the former, and if only the total is shown, it is the latter. If two systems each build theirs without deciding which, reconciliation items of a few won arise every month.

Second, display formats are made in several places. If the screen, the invoice, and the counterparty file each attach decimal points on their own, you get the incident of a decimal point printed on the yen. If you gather parsing and display into one function and make every amount pass through that gate, fixing the currency table in one place is enough.

Third, the rounding policy exists only in code. When the person in charge changes, nobody knows the basis. If you write it in a policy file and carry that value in the report too, you can later explain why this number came out.

What really matters in practice

What you will do in the next lab

You build the fee ledger of 600 rows and the currency table yourself, and count how many lines differ between the same data computed in floating point and computed in integers. Then you reload as minor-unit integers and compare with the REAL copy, gather parsing and display into one file, count the difference between the two rounding modes, build an allocation with the largest remainder method that matches even the leftover, record the difference by conversion order, and finally bundle everything into one report.