Problem z wartościami zmiennoprzecinkowymi polega na tym, że próbują one reprezentować nieskończoną liczbę (ciągłych) wartości za pomocą stałej liczby bitów. Więc oczywiście musi być jakaś strata w grze, a ty ugryziesz się z pewnymi wartościami.
Gdy komputer przechowuje 1,275 jako wartość zmiennoprzecinkową, tak naprawdę nie będzie pamiętał, czy był to 1.275, czy 1.27499999999999993, czy nawet 1.27500000000000002. Wartości te powinny dawać różne wyniki po zaokrągleniu do dwóch miejsc po przecinku, ale nie będą, ponieważ dla komputera wyglądają dokładnie tak samo po zapisaniu jako wartości zmiennoprzecinkowe i nie ma możliwości przywrócenia utraconych danych. Wszelkie dalsze obliczenia będą tylko kumulować taką niedokładność.
Jeśli więc precyzja ma znaczenie, od samego początku należy unikać wartości zmiennoprzecinkowych. Najprostsze opcje to
- użyj oddanej biblioteki
- używaj ciągów do przechowywania i przekazywania wartości (w połączeniu z operacjami ciągów)
- używaj liczb całkowitych (np. możesz podać około setnych swojej rzeczywistej wartości, np. kwota w centach zamiast kwoty w dolarach)
Na przykład, używając liczb całkowitych do przechowywania setnych, funkcja znajdowania rzeczywistej wartości jest dość prosta:
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));
W przypadku ciągów trzeba zaokrąglać, ale nadal jest to możliwe:
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));
Zauważ, że ta funkcja zaokrągla wartość do najbliższej, wiąże się od zera , podczas gdy IEEE 754 zaleca zaokrąglanie do najbliższej, wiąże nawet jako domyślne zachowanie dla operacji zmiennoprzecinkowych. Takie modyfikacje pozostawiają czytelnikowi ćwiczenie :)