I'm building an expense-splitting app for couples. Early on, I stored every amount as a double. It worked until two people split a ₹1,000.05 grocery bill three ways — and the app insisted someone still owed ₹0.01 that nobody could pay.
Why it happens
0.1 + 0.2 === 0.30000000000000004. Binary floating point can't represent most decimal fractions exactly, the same way decimal can't write 1/3. Individually the error is invisible. Across a month of bills, summed and re-split, it surfaces as a phantom rupee.
The fix: integers, always
Store the smallest unit — paise, not rupees:
const toPaise = (rupees) => Math.round(rupees * 100);
const format = (paise) => ₹${(paise / 100).toFixed(2)};
Every amount in the database is an integer. Addition and subtraction are now exact. You convert to a display string at the very last moment, and never compute on that string.
Splitting without losing a paisa
Division is where money actually disappears. ₹1000.05 three ways is 33335 paise each with 0 left, but ₹1000.04 leaves a remainder of 1. Rounding each share independently either invents or destroys money.
Distribute the remainder explicitly instead:
function split(totalPaise, weights) {
const sum = weights.reduce((a, b) => a + b, 0);
const shares = weights.map(w => Math.floor(totalPaise * w / sum));
let left = totalPaise - shares.reduce((a, b) => a + b, 0);
for (let i = 0; left > 0; i = (i + 1) % shares.length, left--) shares[i]++;
return shares;
}
The shares now always sum to exactly the total. Someone absorbs an extra paisa — but it's deliberate, and it's visible.
The rule
Integers for storage and arithmetic. Strings only for display. Never a float, and never a rounded intermediate value.
The bug that started this took an evening to find and three lines to fix. Money is the one place where "close enough" is a bug report.
Top comments (0)