Pengeregning i hele cent, og rabatten som forsvant
Vi ga femti cent i rabatt, og hovedboka registrerte ingenting av det. Flyttall var ikke engang synderen. En feltguide til pengeregning med heltall: klemming, fordeling, og hvordan en hovedbok med enhetspriser summerer seg til nøyaktig riktig total.
Enhver utvikler lærer tidlig at 0,1 pluss 0,2 ikke er 0,3, så vi lagrer alle penger som hele cent og føler oss trygge. Så bygger du noe ekte, som en prosentrabatt over en handlekurv, og oppdager at heltall bare løser det første problemet. Det andre problemet er at divisjon ikke bryr seg om hovedboka di.
De femti centene som forsvant
Her er oppsettet som tok oss, redusert til beinet. En handlekurv har 100 enheter av en vare til én dollar. Kunden får femti cent avslag på ordren. Delsummen er 10 000 cent, rabatten er 50 cent, og den rabatterte totalen er 9 950 cent. Så langt bare heltall.
Så fører du det salget i en hovedbok som lagrer en enhetspris per linje, slik regnskapssystemer liker det. Den rabatterte enhetsprisen er 9 950 delt på 100, altså 99,5 cent. Det er ikke et heltall, så det avrundes til 100. Hovedboka sier nå 100 enheter til 100 cent: et salg til full pris. Rabatten ble ikke mindre. Den forsvant, mens grensesnittet lovet kunden 9 950 og bøkene 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 integerDelingen ser pirkete ut helt til du ser hva den kjøper deg. Femti enheter til 100 cent pluss femti enheter til 99 cent blir nøyaktig 9 950. Hver rad lagrer fortsatt en ærlig enhetspris i hele tall, antallene går fortsatt opp, og summen av hovedboka er lik tallet på kvitteringen til siste cent. Ingenting av det er mulig med én avrundet pris.
Fordeling trenger en avstemmingsrunde
Den samme sykdommen dukker opp ett nivå opp når en rabatt på ordrenivå fordeles over flere linjer. Gi hver linje sin forholdsmessige andel og rund av, så vil andelene summere seg til nesten rabatten. Noen ganger én cent over, noen ganger én under, av og til helt presist, som er det verste utfallet av alle, fordi det skjuler feilen for en overfladisk 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 negativeTo detaljer i den løkka har gjort seg fortjent til plassen den harde veien. Resten fordeles over enhver linje med kapasitet, ikke bare den siste linja, fordi den siste linja kan være en vare til seks cent uten plass til å ta imot tre restcent. Og hver andel begrenses til sin egen linjedelsum, fordi en rabatt større enn linja innebærer en negativ enhetspris, og negative enhetspriser er måten en hovedbok begynner å lyve den andre veien på.

Klem i hver ytterkant
- Rabatter klemmes til området fra null til delsummen. Hundre og ti prosent avslag er en skrivefeil, ikke en refusjon.
- Les brukerangitte beløp defensivt: tomt og ikke-numerisk betyr null, aldri NaN midt i et regnestykke.
- Antall klemmes til det som finnes. Pengekode arver hver eneste lagerfeil oppstrøms for seg.
- Test totalene som ikke går opp. Tre varer til ti dollar med én dollar avslag er en bedre test enn et hvilket som helst rundt tall.
Ingenting av dette er glamorøst, som er nøyaktig derfor det slipper gjennom kodegjennomgangen. Regnestykket ser riktig ut fordi det er riktig, helt til heltallsdivisjon stille kaster bort en rest noen hadde krav på. Skriv ned invarianten, summen av delene er lik helheten, og la en test holde døra.
Heltall holder pengene ærlige. Avstemming holder heltallene ærlige.