JavaScript und die Rundungsfehler im Datentyp Number

Warum JavaScript (scheinbar) nicht rechnen kann
Kommentare

JavaScript hat das geschafft, was Java immer schaffen wollte – es ist überall vertreten. Dennoch gibt es ein kleines Problem: Scheinbar kann JavaScript nicht vernünftig rechnen …

JavaScript ist weit verbreitet und kommt schon längst nicht mehr nur im Browser zum Einsatz. Wer allerdings Rechenoperationen mit großen Zahlen ausführen muss, gelangt schnell an die Grenzen und bekommt es mit Rundungsfehlern zu tun; so auch wir bei Neofonie. Schuld daran ist ein Standard, der von vielen Sprachen verwendet wird. Genau dieser Standard sorgt dafür, dass Entwickler in JavaScript immer wieder auf folgendes Problem stoßen:

var result = 0.1 + 0.2;
result === 0.3
// false !
// result == 0.30000000000000004

In JavaScript existiert für Zahlen nur ein Datentyp: Number.

Der Datentyp Number

Number ist ein 64-Bit-Floating-Point-Datentyp. Diese 64 Bit werden dabei entsprechend dem Standard IEEE 754 (bzw. Double precision binary floating point format) so aufgeteilt, dass die Bits 0 bis 51 für die Mantisse (den eigentlichen Zahlenwert), die Bits 52 bis 62 für den Exponenten und das letzte Bit (63) für das Vorzeichen verwendet werden.

Jede Zahl wird also in einer binären Exponential-Repräsentation abgebildet. Für Ganzzahl-Werte bedeutet dies, dass eine gesicherte Darstellung bis 15 Ziffern möglich ist, bevor Rundungsfehler bedingt durch die exponentielle Abbildung auftreten.

var x = 999999999999999;   // 999999999999999
var y = 9999999999999999;  // 10000000000000000

Bei Fließkommazahlen wirkt sich dies entsprechend auf die Genauigkeit der Nachkommastellen aus (s. erstes Beispiel).

Nicht nur JavaScript …

Wer sich nun in seiner Meinung bestätigt sieht, dass JavaScript eine total verkorkste Sprache ist, die nicht einmal ordentlich rechnen kann, wird erstaunt sein, dass dieses Problem nicht allein auf JavaScript beschränkt ist. Alle Sprachen, die Fließkommazahlen nach IEEE 754 implementieren, weisen entsprechende Rundungsfehler auf. Der Unterschied besteht lediglich in den diversen Sprachen sich unterscheidenden zusätzlichen Datentypen.

Wer es nicht glaubt, kann gerne einmal in Groovy (bzw. Java) folgendes ausprobieren:

println "0.1 + 0.2 = ${0.1 + 0.2}" // "0.1 + 0.2 = 0.3"
println "0.1f + 0.2f = ${0.1f + 0.2f}" // "0.1f + 0.2f = 0.30000000447034836"
println "0.1d + 0.2d = ${0.1d + 0.2d}" // "0.1d + 0.2d = 0.30000000000000004"

Daran lässt sich auch gut erkennen, dass Number dem Datentyp „Double“ in Java entspricht.

Eine Anekdote am Rande

Wem das alles nun viel zu kompliziert erscheint, der ist an dieser Stelle sicher nicht alleine. Wichtig ist es zu wissen, dass Rundungsfehler aufgrund der Abbildung entstehen können.

Selbst ein so genialer Kopf wie Steve Wozniak hat es vermieden, einen Floating-Point-Datentyp in Apples erstem Basic zu implementieren. Laut Wikipedia war Wozniak zu sehr mit der Entwicklung des Diskettenlaufwerks beschäftigt, weshalb Microsoft beauftragt wurde, ein Basic mit Fließkommazahlen zu entwickeln (später genannt „Applesoft Basic“).

Stellen Sie Ihre Fragen zu diesen oder anderen Themen unseren entwickler.de-Lesern oder beantworten Sie Fragen der anderen Leser.

Sicher hätte Wozniak die intellektuellen Kapazitäten dazu besessen, das Problem zügig zu lösen. Witzig ist jedoch, dass es nie eine offizielle Begründung gab, warum Wozniak damals keine Fließkommazahlen in Apple Basic umgesetzt hat. Während eines Interviews bei All Things Digital (2007) sagt Jobs auf die Frage, warum Wozniak keine Fließkommazahlen in Apple Basic umsetzte, lediglich: „This is one of the mysteries of life“.

Wie vermeidet man Rundungsfehler?

Ist man gezwungen innerhalb von JavaScript mit großen Zahlen zu rechen und sucht etwas Analoges zu Javas BigDecimal, sollte man sich vielleicht einmal bignumber.js oder BigDecimal.js anschauen. Für den gewöhnlichen Gebrauch genügt es meist jedoch, sich auf drei bis vier Stellen nach dem Komma zu beschränken und die nativen Methoden numObj.toPrecision(Stelligkeit) oder numObj.toFixed(Nachkommastellen) zu verwenden. Der Unterschied zwischen beiden ist, dass man bei toPrecision() die Gesamtanzahl der darzustellenden Ziffern (Vorkomma + Nachkomma) angibt, während man bei toFixed() die Anzahl der darzustellenden Nachkommastellen angibt.

var result = parseFloat((0.1 + 0.2).toPrecision(1));
result === 0.3
// true

Zu beachten sei noch, dass sowohl .toFixed() als auch .toPrecision() als Rückgabewert kein Number-Objekt sondern einen String liefern, weshalb in dem oberen Beispiel auch noch einmal ein parseFloat() nötig ist, um auch einem Typvergleich (===) standzuhalten.

Neben einer Berechnung wie zuvor gesehen, ist auch bei der Übertragung von Zahlen darauf zu achten, ob diese eventuell implizit zu Number konvertiert werden. So interpretiert beispielsweise jQuery.data() den im Data-Attribut des angefragten Elements enthaltenen Wert. Ist dieser ein Zahlenwert, wird er entsprechend als Number zurückgegeben. Das ist solange kein Problem, wie der Wertebereich Double eingehalten wird. Oft werden jedoch IDs auf diese Weise übertragen, die wiederum gerne Long-Werte sind und daher auch eine Stelligkeit jenseits der gesicherten 15 Stellen erreichen können. Um eine implizite Umwandlung zu vermeiden, sollte man daher auf das Data-Attribut, welches einen Long-Wert enthält, zur Sicherheit mit der Methode jQuery.attr() zugreifen, da sie stets einen String zurückliefert.

Wer das Problem der Rundungsgenauigkeit beim Abbilden von Fließkommazahlen in eine binäre Exponentialdarstellung selber einmal im Code lösen möchte, kann sich am folgenden Coding Kata versuchen: Codewars – Float to binary conversion (wenn beim ersten Pageload eine Fehlermeldung erscheint, einfach mit einem Reload erneut versuchen).

Weiterführende Links

 

Aufmacherbild: carrots and school mathematics with math problems von Shutterstock / Urheberrecht: Laborant

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -