14 maart is een onofficiële feestdag in de programmeerwereld: Pi-dag (3.14). Een uitgelezen moment om het te hebben over hoe computers met gebroken getallen omgaan, waarom rekenuitkomsten er soms onverwacht uitzien, en welke gevolgen dat in echte projecten kan hebben — zeker wanneer er geld in het spel is.
Het experiment dat elke beginner verrast
Open een interactieve console in welke programmeertaal dan ook en evalueer de eenvoudigst denkbare uitdrukking:
0.1 + 0.2
Dart, JavaScript, Python, Go, C++, Ruby — de uitkomst is steeds dezelfde: 0.30000000000000004. Niet 0.3, zoals het gezond verstand zou ingeven, maar een getal met een lange staart nullen en een vier aan het eind.
Dit is geen bug in een bepaalde taal, en ook geen fout in jouw code. Het is een fundamentele eigenschap van hoe moderne hardware gebroken getallen in het geheugen weergeeft.
Hoe computers gebroken getallen opslaan
Alle informatie in een computer wordt binair opgeslagen — reeksen nullen en enen. Voor hele getallen werkt dat feilloos: 5 is binair 101, 10 is 1010. Elk heel getal heeft een exacte binaire weergave.
Gebroken getallen zijn een fundamenteel ander verhaal. Om een decimale breuk naar binair om te zetten, vermenigvuldigen we herhaaldelijk met 2 en nemen we het gehele deel. Kijk wat er met 0,1 gebeurt:
0.1 × 2 = 0.2 → 0
0.2 × 2 = 0.4 → 0 ← cyclus begint
0.4 × 2 = 0.8 → 0
0.8 × 2 = 1.6 → 1
0.6 × 2 = 1.2 → 1 ← cyclus eindigt
0.2 × 2 = 0.4 → 0 ← cyclus herhaalt
...
Het resultaat is 0,0(0011) — een oneindig repeterende binaire breuk. Dat is vergelijkbaar met hoe 1/3 in decimalen de oneindige 0,3333... wordt — onmogelijk om exact in een eindig aantal cijfers op te schrijven.
Het getal 0,2 loopt tegen hetzelfde probleem aan. Beide getallen in onze optelling bevatten al een afrondfout op het moment dat ze in het geheugen worden gezet.
De IEEE 754-standaard: een afweging tussen precisie en prestaties
De overgrote meerderheid van moderne processors gebruikt de IEEE 754-standaard voor rekenen met drijvende komma. Het type double (64-bits dubbele precisie), dat in de meeste programmeertalen de standaard is, is als volgt opgebouwd:
- 1 bit voor het teken (positief of negatief)
- 11 bits voor de exponent (de orde van grootte)
- 52 bits voor de mantisse (de significante cijfers)
52 bits mantisse komt neer op ongeveer 15 tot 17 significante decimale cijfers. Is de binaire weergave van een breuk oneindig, dan wordt hij bij de 52e bit afgekapt. Dat is precies het moment waarop de weergavefout ontstaat.
Tel je twee getallen op die elk een afrondfout bevatten, dan stapelen die fouten zich op. Zo wordt 0.1 + 0.2 gelijk aan 0.30000000000000004.
Wanneer de fout acceptabel is
Eerlijk is eerlijk: voor de meeste taken is die fout volstrekt verwaarloosbaar. We hebben het over een fout in de orde van 10⁻¹⁶ — een biljardste.
double werkt prima in de volgende domeinen:
- Computergraphics en renderen — een verschil op de zestiende decimaal is voor het menselijk oog onzichtbaar
- Natuurkundige simulaties in games — objecten gedragen zich realistisch en de fout heeft geen merkbaar effect
- Wetenschappelijk rekenwerk — uitkomsten worden toch meestal op de gevraagde precisie afgerond
- Statistiek en machine learning — modellen werken van nature met benaderingen
Wanneer de fout kritiek wordt: financiële berekeningen
De zaak verandert ingrijpend zodra er geld in het spel is. In financiële systemen moet elke cent exact kloppen. Een fout op de zestiende decimaal lijkt voor één transactie onbeduidend, maar wanneer je duizenden en miljoenen bewerkingen verwerkt, stapelen die microscopische afrondfouten zich op tot echte verschillen in saldi.
Neem een concreet voorbeeld. Stel dat een systeem 100.000 transacties per dag verwerkt, en dat elke daarvan een fout van 0,000000000000004 introduceert. Per dag is dat 0,0000000004 — een verwaarloosbaar bedrag. Maar vermenigvuldig dat met een jaar, voeg complexere bewerkingen met vermenigvuldigen en delen toe waarbij fouten veel sneller groeien, en je eindigt met bedragen die noch een boekhouder noch een accountant kan verklaren.
Betrouwbare manieren om met geldbedragen om te gaan
Aanpak één: kleinste eenheden als hele getallen
Het idee is rechttoe rechtaan: in plaats van een bedrag in euro's als gebroken getal op te slaan, sla je het op in de kleinste munteenheid (centen) als heel getal.
1999 cent is altijd precies € 19,99. Geen afronding, geen verrassingen, geen 0,000000004 die erachteraan sleept. Rekenen met hele getallen is in computers absoluut exact.
Deze aanpak wordt breed gebruikt in betaalsystemen. Stripe verwerkt bijvoorbeeld alle bedragen uitsluitend in de kleinste munteenheid.
Het is de moeite waard een alternatief voor API's te noemen: geldbedragen als tekenreeks doorgeven. In dat geval blijft de interface leesbaar voor mensen ("19.99" is duidelijker dan 1999), terwijl de verantwoordelijkheid voor het ontleden en het kiezen van het juiste numerieke type bij de client komt te liggen.
Aanpak twee: bibliotheken met willekeurige precisie (BigDecimal)
De tweede betrouwbare optie is gespecialiseerde datatypes gebruiken die decimale getallen opslaan zonder ze naar binair om te zetten:
- Dart — het pakket
decimal - Java —
java.math.BigDecimal - Python — de module
decimaluit de standaardbibliotheek - JavaScript — het voorstel Decimal is in behandeling; tot die tijd bibliotheken als
decimal.js - Go — het pakket
shopspring/decimal
Deze types stellen getallen in decimale vorm voor en vermijden binaire benaderingen volledig. De uitdrukking 0.1 + 0.2 == 0.3 geeft met BigDecimal gegarandeerd true terug.
De prijs voor die precisie is snelheid: bewerkingen met BigDecimal zijn aanzienlijk trager dan met double. Maar in de context van financiële berekeningen, waar correctheid zwaarder weegt dan snelheid, is dat een volstrekt aanvaardbare afweging.
Welke aanpak je kiest
Beide aanpakken lossen het probleem op, maar ze passen bij verschillende situaties:
| Criterium | Kleinste eenheden (int) | BigDecimal |
|---|---|---|
| Prestaties | Maximaal | Lager |
| Eenvoud van uitvoering | Hoog | Gemiddeld (hangt van de taal af) |
| Ondersteuning voor deeleenheden | Nee (alleen hele getallen) | Ja |
| Ondersteuning voor meerdere valuta | Vraagt kennis van het aantal decimalen | Werkt direct |
Voor de meeste e-commerce- en betaalsystemen is de aanpak met kleinste eenheden optimaal. BigDecimal verdient de voorkeur in situaties die tussentijdse gebroken berekeningen vragen — bijvoorbeeld rente, belasting of valutaomrekening.
Conclusie
Werkt je project met geldbedragen, houd je dan aan een van twee regels:
- Sla bedragen op en geef ze door in de kleinste eenheid als heel getal (
int) - Gebruik types met willekeurige precisie (
BigDecimalen zijn tegenhangers)
double gebruiken voor financiële berekeningen is technische schuld die zich maandenlang niet hoeft te manifesteren, maar uiteindelijk tot verschillen leidt waarvan de oorzaak buitengewoon lastig te achterhalen is.
Kies gereedschap dat bij de taak past, en let op hoe je programmeertaal getallen in het geheugen weergeeft. Dit is een van die dingen die je beter kunt weten voordat het een productie-incident wordt.
Precies deze afwegingen maken wij bij het bouwen van apps die met geld omgaan — ons fintechwerk aan ExtraETF en onze bredere praktijk Flutter-appontwikkeling leunen allebei op deze regels.
Fijne Pi-dag! 🥧

