Das Problem mit Gleitkommawerten besteht darin, dass sie versuchen, eine unendliche Anzahl von (kontinuierlichen) Werten mit einer festen Anzahl von Bits darzustellen. Natürlich muss es einen Verlust im Spiel geben, und Sie werden mit einigen Werten gebissen.
Wenn ein Computer 1,275 als Gleitkommawert speichert, merkt er sich nicht, ob es 1,275 oder 1,27499999999999993 oder sogar 1,27500000000000002 war. Diese Werte sollten nach dem Runden auf zwei Dezimalstellen unterschiedliche Ergebnisse liefern, werden dies jedoch nicht, da sie für Computer nach dem Speichern als Gleitkommawerte genau gleich aussehen und es keine Möglichkeit gibt, die verlorenen Daten wiederherzustellen. Bei weiteren Berechnungen wird nur eine solche Ungenauigkeit akkumuliert.
Wenn es also auf Präzision ankommt, müssen Sie Gleitkommawerte von Anfang an vermeiden. Die einfachsten Möglichkeiten sind zu
- Verwenden Sie eine dedizierte Bibliothek
- Verwenden Sie Zeichenfolgen zum Speichern und Weitergeben der Werte (begleitet von Zeichenfolgenoperationen).
- Verwenden Sie ganze Zahlen (z. B. könnten Sie den Betrag von Hundertstel Ihres tatsächlichen Werts herumgeben, z. B. den Betrag in Cent anstelle des Betrags in Dollar).
Wenn Sie beispielsweise Ganzzahlen zum Speichern der Anzahl der Hundertstel verwenden, ist die Funktion zum Ermitteln des tatsächlichen Werts recht einfach:
function descale(num, decimals) {
var hasMinus = num < 0;
var numString = Math.abs(num).toString();
var precedingZeroes = '';
for (var i = numString.length; i <= decimals; i++) {
precedingZeroes += '0';
}
numString = precedingZeroes + numString;
return (hasMinus ? '-' : '')
+ numString.substr(0, numString.length-decimals)
+ '.'
+ numString.substr(numString.length-decimals);
}
alert(descale(127, 2));
Bei Strings müssen Sie runden, aber es ist immer noch überschaubar:
function precise_round(num, decimals) {
var parts = num.split('.');
var hasMinus = parts.length > 0 && parts[0].length > 0 && parts[0].charAt(0) == '-';
var integralPart = parts.length == 0 ? '0' : (hasMinus ? parts[0].substr(1) : parts[0]);
var decimalPart = parts.length > 1 ? parts[1] : '';
if (decimalPart.length > decimals) {
var roundOffNumber = decimalPart.charAt(decimals);
decimalPart = decimalPart.substr(0, decimals);
if ('56789'.indexOf(roundOffNumber) > -1) {
var numbers = integralPart + decimalPart;
var i = numbers.length;
var trailingZeroes = '';
var justOneAndTrailingZeroes = true;
do {
i--;
var roundedNumber = '1234567890'.charAt(parseInt(numbers.charAt(i)));
if (roundedNumber === '0') {
trailingZeroes += '0';
} else {
numbers = numbers.substr(0, i) + roundedNumber + trailingZeroes;
justOneAndTrailingZeroes = false;
break;
}
} while (i > 0);
if (justOneAndTrailingZeroes) {
numbers = '1' + trailingZeroes;
}
integralPart = numbers.substr(0, numbers.length - decimals);
decimalPart = numbers.substr(numbers.length - decimals);
}
} else {
for (var i = decimalPart.length; i < decimals; i++) {
decimalPart += '0';
}
}
return (hasMinus ? '-' : '') + integralPart + (decimals > 0 ? '.' + decimalPart : '');
}
alert(precise_round('1.275', 2));
alert(precise_round('1.27499999999999993', 2));
Beachten Sie, dass diese Funktion auf den nächsten Wert rundet und von Null abweicht , während IEEE 754 empfiehlt, auf den nächsten Wert zu runden, der sogar als Standardverhalten für Gleitkommaoperationen verwendet wird. Solche Modifikationen bleiben dem Leser als Übung überlassen :)