Why business money should be integer paise, not a float
Binary floating point cannot hold most decimal fractions, so rupee arithmetic drifts unpredictably. Why display rounding hides it, and what paise fix.
· 4 min read
What a float actually is, and why decimal rupees do not fit it
Almost every programming language and spreadsheet represents an ordinary decimal number internally in binary floating point, the IEEE-754 standard. Binary floating point can represent some decimal fractions exactly and cannot represent most of them exactly at all, for the same reason a third cannot be written exactly in decimal — 0.333... never terminates. Many ordinary two-decimal rupee amounts fall into the group that cannot be represented exactly in binary.
This is not a hypothetical edge case. It is demonstrable in the exact runtime this product's own code runs on: in JavaScript, 0.1 + 0.2 does not equal 0.3. It equals 0.30000000000000004. Nothing about that number was mistyped or misconfigured. It is what happens when two numbers that cannot be represented exactly in binary are added together and the tiny representation error in each one does not cancel out.
Why this looks fine for a long time
Most money software displays a figure through a rounding function like toFixed(2), which rounds the stored value to two decimal places for display. That step hides small errors from view without fixing them in the stored number underneath — the value that later arithmetic will actually use is still the unrounded, slightly wrong one. And not every float operation on money produces a visible artifact: 19.99 multiplied by 3 comes out to exactly 59.97 in the same runtime that gets 0.1 plus 0.2 wrong. Whether a given calculation drifts or not depends on the specific numbers involved, in a way that is not predictable by looking at the inputs.
That unpredictability is what makes this genuinely dangerous rather than a curiosity. A business cannot look at its own formulas and know in advance which ones are safe and which are quietly accumulating error, because the same operation is safe for some amounts and not for others. The error does not announce itself at the moment it happens. It waits.
Where it actually surfaces
The place a small, silent float error tends to surface is exactly the place this series has already covered: reconciliation. A books total computed through a chain of float additions, subtractions and tax-rate multiplications can drift from a total computed a different way — say, the bank's own arithmetic — by a paisa or a few paise. By the time someone notices the mismatch, often months later during a reconciliation or a year-end close, there is no log of which of the hundreds or thousands of intervening operations introduced the drift. The error was never recorded as an event. It was baked silently into a stored number, indistinguishable from every other digit sitting next to it.
This is what makes a float-based rounding error categorically different from an ordinary data-entry mistake. A wrong invoice amount can usually be traced back to the invoice. A float drift has no single point of origin to trace back to — it is the accumulated residue of arithmetic, and by the time it is visible, the individual operations that caused it are long gone.
What an integer count of minor units fixes
The alternative this product's own money-handling code uses, in lib/money.ts and the backends behind Laxmi, Khata and Dena, is to never store a rupee amount as a decimal number at all. Every amount is an integer count of the currency's minor unit — paise for rupees — sitting beside an explicit currency code. An amount of fifteen thousand rupees is not stored as 15000.00; it is stored as the integer 1500000, meaning one million five hundred thousand paise, formatted for display only at the very last step.
Integer arithmetic does not have the representation problem binary floating point has for decimal fractions — adding, subtracting and comparing whole numbers is exact, every time, regardless of how many operations are chained together. Formatting happens once, at render time, by working directly on the digits of the integer as a string — splitting off the last two digits as paise and grouping the rest — rather than dividing by 100 and hoping the result rounds cleanly. A number like formatMinor(1500000, "INR") always produces exactly INR 15,000.00, with no path through a float division at any point. Khata's ledger and Laxmi's invoices and bills are both built on exactly this rule — automixai.in/docs/khata and automixai.in/docs/laxmi both state it as a hard constraint on every amount they store, not a formatting preference applied only where convenient.
Why the currency column matters as much as the integer
An integer alone is not a complete answer, because how many digits count as the minor unit differs by currency. Most currencies use two decimal places for their minor unit, but not all of them — the Japanese yen has none, and the Bahraini dinar has three. Formatting an amount without knowing which currency it is in risks getting the decimal placement wrong by a factor of ten or a hundred, which is a much larger and more dangerous error than a rounding artifact.
This is why a properly built money-formatting function refuses to guess a currency rather than defaulting to one. A workspace that holds both rupee and dirham balances cannot safely assume every bare number it sees is rupees, and a function that quietly assumed INR whenever the currency was missing would eventually format a dirham amount as if it were a hundred times smaller than it actually is, with nothing in the output to flag that anything went wrong.
The aggregation rule that follows from all of this
One consequence of taking currency this seriously is that a properly built money layer will not offer a single function that sums everything into one number regardless of currency. Adding a rupee figure to a dirham figure and presenting the sum as a single total produces a number with no real unit, stated with complete confidence, and nothing downstream can tell that it happened — the figure looks exactly as legitimate as a real, single-currency total. The only aggregation this kind of system should offer is a total per currency, never a blended figure, because a blended figure is not a rounding error waiting to be found later — it is wrong the moment it is created.
Common questions
Is this only a software problem, or does it matter for a business that keeps books in a spreadsheet?
Spreadsheet software also represents ordinary number cells in binary floating point internally, the same representation this article describes. Most spreadsheet formatting hides small errors well enough that they are rarely visible day to day, but formatting a cell for display does not change the underlying stored value, so the same class of drift is possible with enough operations. If a total needs to be exact to the paisa, the safer approach is the same one described here: work in whole paise and divide only for display.
If the error is this small, does it actually matter for a small business?
A single instance is genuinely tiny — often a fraction of a paisa that display rounding absorbs without anyone noticing. What makes it worth avoiding structurally is that it compounds silently across thousands of transactions and cannot be traced back to its source once it surfaces, unlike an ordinary data-entry mistake that can usually be found by checking the original document.
Why not just round every float result to two decimal places after each calculation?
Rounding after each step reduces the visible symptom but does not remove the underlying representation problem, and it introduces a new question — exactly when and how each intermediate rounding happens — that itself has to be applied consistently everywhere money is touched, or the same class of drift reappears through a different door. Working entirely in integer minor units avoids needing that discipline in the first place, because there is no fractional representation to round.
Does this mean a business should never see a decimal rupee amount?
No — decimal rupee figures are exactly what a person should see on a screen or a printed statement. The point is where the decimal appears: only at the final formatting step, computed from an exact integer count of paise, rather than carried through every calculation along the way as a binary fraction.