Onnauwkeurigheden bij rekenen met JavaScript
De manier van bewaren (dat heet "representatie") van getallen in JavaScript zorgt er bij gebroken
getallen vaak voor dat er afrondingsfouten ontstaan in de laatste decimalen. Bij breuken als ½ en ¼ zijn er
geen problemen, omdat die getallen exact in een bitpatroon zijn te vangen.
Een ander verhaal wordt het als je gaat werken met getallen die je níet exact in een bitpatroon kunt vangen, zoals ⅓.
Op zich is dat geen probleem, want de 16 beschikbare decimalen worden gewoonlijk afgerond naar hoogstens vier plaatsen achter
de komma. Het wordt pas lastig als je veel berekeningen achter elkaar gaat maken. Dit wordt toegelicht aan de hand van deze
formule:
getal = (n + 1)×getal - 1, waarbij getal = 1 / n.
Kijk eens naar onderstaande JavaScript-code, waar de formule wordt berekend voor n = 3:
var i, getal = new Number();
getal = 1/3;
document.getElementById('numbers').innerHTML = getal.toFixed(16) + "0<br>";
for (i=1; i<=30; i++) {
getal = 4 * getal - 1;
document.getElementById('numbers').innerHTML += getal.toFixed(16) + "<br>";
}
In deze code wordt getal ingesteld op 1/3. Dat wordt vermenigvuldigd met 4 (getal is nu 4/3). Vervolgens wordt er 1 van af getrokken waardoor getal weer 1/3 is. Dit wordt 30 keer gedaan. De uitvoer wordt in een HTML-element met id="numbers" gezet.
De uitvoer zie je hiernaast. De eerste stap lijkt goed te gaan, maar dat is niet zo. Al bij het begin (stap 0) is er al een afwijking in de 17e decimaal, maar door de afronding op 16 decimalen zie je dat nog niet. Bij stap 2 wordt een afwijking zichtbaar, daarna komen de afwijkingen snel naar voren, waarna de de boel na stap 22 compleet ontploft en er van enige nauwkeurigheid geen sprake meer is…
In de dagelijkse praktijk zul je hier geen last van hebben. Dit soort berekeningen zul je met een script-taal als JavaScript niet vaak maken. Maar dit effect treedt niet alleen op bij JavaScript, maar bij elke programmeertaal. Het ligt nl. niet aan de taal, maar aan de manier waarop gebroken getallen in een computer worden opgeslagen.
Het gaat in het volgende vooral over het deel van gebroken getallen achter de komma. Het deel dat voor de komma staat heeft dit probleem niet, omdat dat in feite een geheel getal is.
De 'normale' manier om getallen weer te geven is in het 10-tallige stelsel. Een getal wordt weergegeven als een serie machten
van 10, bijvoorbeeld:
1234.567 is eigenlijk 1×103 + 2×102 + 3×101 + 4×100
+ • + 5×10-1 + 6×10-2 + 7×10-3.
In het binaire (tweetallige) stelsel, dat computers gebruiken, worden getallen voorgesteld als machten van twee.
1234.567 wordt dan 210 + 27 +26 + 24 + 21 + • + 2-1 + 2-4+ 2-8+ 2-11+ 2-14+ 2-15 + ...... Dit is niet exact, daarom staan de puntjes er bij.
Het bitpatroon is: 100 1101 0010 • 1001 0001 0010 011. Als je dit terugrekent naar decimaal kom je uit op 1234.56698608398. Geen wereldschokkend verschil met het origineel, maar de afwijking is groot genoeg om een berekening waar dit getal herhaaldelijk in wordt gebruikt onnauwkeurig te maken.
De oorzaak: repeterende breuken, zoals 1/3 (= 0.333333333.... en 1/11(= 0.0909090...), kunnen niet als een reeks machten van twee worden geschreven. Dat geldt ook voor oneindig doorlopende breuken, zoals in 1234.567.
De vraag rijst of er een oplossing zou kunnen zijn voor de afwijkingen in de representatie van gebroken getallen, in een wiskundig of rekenkundig betoog, of in een computergeheugen. Al meer dan 150 jaar breken wiskundigen¹) zich het hoofd over een oplossing. De oplossingen die zijn gevonden hebben allemaal hun eigen voor- en nadelen, en die niet allemaal even bruikbaar zijn in een computerprogramma omdat ze nogal veel rekenkracht vragen. Een van die methodes wordt beschreven in een apart item, zie "Breuken zonder fouten".
¹) Een van die wiskundingen is Georg Cantor (1845 - 1918), bekend van de naar hem genoemde fractal.