Rekenen met hele centen, en de korting die verdween
We gaven vijftig cent korting en het grootboek legde er niets van vast. Floating point was niet eens de boosdoener. Een veldgids voor rekenen met geld in hele getallen: begrenzen, verdelen, en een grootboek van stuksprijzen precies op het totaal laten uitkomen.
Elke ontwikkelaar leert vroeg dat 0.1 plus 0.2 geen 0.3 is, dus slaan we geld allemaal op als hele centen en voelen we ons veilig. Dan bouw je iets echts, zoals een procentuele korting over een winkelmandje, en ontdek je dat hele getallen alleen het eerste probleem oplossen. Het tweede probleem is dat delen niets om je grootboek geeft.
De vijftig cent die zoekraakte
Dit is de opzet die ons beet, teruggebracht tot het bot. Een mandje bevat 100 stuks van een artikel van een dollar. De klant krijgt vijftig cent van de bestelling af. Het subtotaal is 10.000 cent, de korting is 50 cent, en het totaal na korting is 9.950 cent. Tot zover niets anders dan hele getallen.
Leg die verkoop nu vast in een grootboek dat een stuksprijs per regel opslaat, zoals boekhoudsystemen dat graag doen. De stuksprijs na korting is 9.950 gedeeld door 100, oftewel 99,5 cent. Dat is geen heel getal, dus wordt het afgerond naar 100. Het grootboek zegt nu 100 stuks van 100 cent: een verkoop tegen de volle prijs. De korting kromp niet. Hij verdween, terwijl het scherm de klant 9.950 beloofde en de boeken 10.000 zeiden.
// The trap: one rounded unit price cannot represent every total.const subtotal = 100 * 100; // 10000cconst discounted = subtotal - 50; // 9950cconst unit = Math.round(discounted / 100); // 100c ... the discount is goneconsole.log(unit * 100); // 10000c, not 9950c // The fix: split the line into two prices one cent apart.const qty = 100;const base = Math.floor(discounted / qty); // 99cconst rem = discounted - base * qty; // 50 units get one extra cent// rem units at (base + 1), the rest at base:// 50 * 100 + 50 * 99 === 9950 ✓ exact, and every price is an integerDie splitsing lijkt pietluttig tot je ziet wat hij oplevert. Vijftig stuks van 100 cent plus vijftig stuks van 99 cent is precies 9.950. Elke regel slaat nog steeds een eerlijke hele stuksprijs op, de aantallen kloppen nog steeds, en de som van het grootboek is tot op de cent gelijk aan het bedrag op de bon. Niets daarvan kan met één afgeronde prijs.
Verdelen vraagt om een sluitronde
Dezelfde kwaal duikt een niveau hoger op als een korting op de hele bestelling zich over meerdere regels verspreidt. Geef elke regel zijn evenredige aandeel en rond af, en de aandelen tellen op tot bijna de korting. Soms een cent te veel, soms een cent te weinig, af en toe precies goed, en dat is de slechtste uitkomst van allemaal, want het verbergt de bug voor wie er losjes op test.
// Prorate by share, then push the rounding leftover onto lines with room.let allocated = 0;for (const line of lines) { line.discount = Math.min( line.subtotal, Math.round((discount * line.subtotal) / orderSubtotal), ); allocated += line.discount;}let remainder = discount - allocated; // may be negativefor (const line of lines) { if (remainder === 0) break; const room = remainder > 0 ? line.subtotal - line.discount : line.discount; const move = Math.min(Math.abs(remainder), room) * Math.sign(remainder); line.discount += move; remainder -= move;}// invariant: sum(line.discount) === discount, and no line goes negativeTwee details in die lus hebben zich op de harde manier bewezen. De rest wordt verdeeld over elke regel met ruimte en niet alleen over de laatste, want de laatste regel kan een artikel van zes cent zijn dat geen drie overgebleven centen kan opnemen. En elk aandeel wordt afgetopt op het subtotaal van de eigen regel, want een korting die groter is dan de regel betekent een negatieve stuksprijs, en met negatieve stuksprijzen begint een grootboek de andere kant op te liegen.

Begrens aan elke rand
- Kortingen begrens je tussen nul en het subtotaal. Honderdtien procent korting is een typefout, geen terugbetaling.
- Lees door gebruikers ingevoerde bedragen defensief uit: leeg en niet-numeriek betekenen nul, nooit NaN midden in een som.
- Aantallen begrens je tot wat er is. Code die met geld rekent erft elke voorraadbug die erboven zit.
- Test de totalen die niet netjes delen. Drie artikelen van tien dollar met een dollar korting is een betere test dan welk rond getal ook.
Niets hiervan is glamoureus, en precies daarom glipt het door de review. Het rekenwerk ziet er correct uit omdat het correct is, tot het moment dat een deling met hele getallen stilletjes een rest weggooit waar iemand recht op had. Schrijf de regel op, de som van de delen is gelijk aan het geheel, en zet er een test voor de deur.
Hele getallen houden geld eerlijk. De sluitronde houdt hele getallen eerlijk.