Pengaräkning i heltalscent, och rabatten som försvann
Vi gav femtio cents rabatt och huvudboken registrerade inget av den. Flyttal var inte ens boven. En fältguide till pengaräkning med heltal: att begränsa värden, fördela rabatter proportionellt och få en huvudbok med styckpriser att summera till exakt totalsumman.
Varje utvecklare lär sig tidigt att 0,1 plus 0,2 inte är 0,3, så vi lagrar alla pengar som heltalscent och känner oss trygga. Sedan bygger man något på riktigt, som en procentrabatt över en varukorg, och upptäcker att heltal bara löser det första problemet. Det andra problemet är att division inte bryr sig om din huvudbok.
De femtio cent som försvann
Här är uppställningen som bet oss, nedkokad till benet. En varukorg innehåller 100 enheter av en vara som kostar en dollar. Kunden får femtio cent av på ordern. Delsumman är 10 000 cent, rabatten är 50 cent, och den rabatterade totalsumman är 9 950 cent. Så långt inget annat än heltal.
Bokför nu den försäljningen i en huvudbok som lagrar ett styckpris per rad, så som redovisningssystem gillar. Det rabatterade styckpriset är 9 950 delat med 100, vilket är 99,5 cent. Det är inget heltal, så det avrundas till 100. Huvudboken säger nu 100 enheter à 100 cent: en försäljning till fullpris. Rabatten krympte inte. Den försvann, medan gränssnittet lovade kunden 9 950 och böckerna sa 10 000.
// 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 integerUppdelningen ser petig ut tills man ser vad den ger. Femtio enheter à 100 cent plus femtio enheter à 99 cent blir exakt 9 950. Varje rad lagrar fortfarande ett ärligt styckpris i heltal, antalen går fortfarande ihop, och summan av huvudboken är lika med siffran på kvittot ner till cent. Inget av det är möjligt med ett enda avrundat pris.
Proportionering behöver en avstämningsrunda
Samma sjukdom dyker upp ett steg upp när en rabatt på ordernivå sprids över flera rader. Ge varje rad sin proportionella andel och avrunda, så summerar andelarna till nästan rabatten. Ibland en cent över, ibland en under, då och då precis rätt, vilket är det värsta utfallet av alla eftersom det döljer buggen för slentrianmässig testning.
// 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 negativeTvå detaljer i den loopen förtjänade sin plats den hårda vägen. Överskottet sprids över vilken rad som helst med utrymme, inte bara den sista raden, eftersom den sista raden kan vara en vara på sex cent utan utrymme att svälja tre överblivna cent. Och varje andel begränsas av radens egen delsumma, eftersom en rabatt större än raden innebär ett negativt styckpris, och negativa styckpriser är hur en huvudbok börjar ljuga åt andra hållet.

Begränsa i varje kant
- Rabatter begränsas till intervallet från noll till delsumman. Hundratio procent av är ett skrivfel, inte en återbetalning.
- Tolka belopp som användaren skrivit in defensivt: tomt och icke-numeriskt betyder noll, aldrig NaN mitt i en uträkning.
- Antal begränsas till det som finns. Pengakod ärver varje lagerbugg uppströms.
- Testa totalerna som inte delar sig jämnt. Tre varor à tio dollar med en dollar av är ett bättre test än något jämnt tal.
Inget av det här är glamoröst, vilket är precis därför det slinker igenom granskningen. Uträkningen ser korrekt ut eftersom den är korrekt, ända tills heltalsdivision tyst slänger bort en rest som någon hade rätt till. Skriv ner invarianten, summan av delarna är lika med helheten, och låt ett test hålla dörren.
Heltal håller pengar ärliga. Avstämning håller heltalen ärliga.