O problema com os valores de ponto flutuante é que eles estão tentando representar uma quantidade infinita de valores (contínuos) com uma quantidade fixa de bits. Então, naturalmente, deve haver alguma perda no jogo, e você será mordido com alguns valores.
Quando um computador armazena 1.275 como um valor de ponto flutuante, na verdade não se lembra se era 1.275 ou 1.27499999999999993 ou mesmo 1.27500000000000002. Esses valores devem fornecer resultados diferentes após o arredondamento para duas casas decimais, mas não o serão, pois, para o computador, parecem exatamente os mesmos após o armazenamento como valores de ponto flutuante, e não há como restaurar os dados perdidos. Quaisquer cálculos adicionais acumularão essa imprecisão.
Portanto, se a precisão importa, você deve evitar valores de ponto flutuante desde o início. As opções mais simples são:
- use uma biblioteca dedicada
- use cadeias de caracteres para armazenar e passar os valores (acompanhados de operações de cadeia de caracteres)
- use números inteiros (por exemplo, você pode passar a quantia de centésimos do seu valor real, por exemplo, quantia em centavos em vez da quantia em dólares)
Por exemplo, ao usar números inteiros para armazenar o número de centésimos, a função para encontrar o valor real é bastante simples:
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));
Com seqüências de caracteres, você precisará arredondar, mas ainda é gerenciável:
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));
Observe que esta função arredonda para o mais próximo, desvia do zero , enquanto o IEEE 754 recomenda arredondar para o mais próximo, desvia para o mesmo como o comportamento padrão para operações de ponto flutuante. Tais modificações são deixadas como um exercício para o leitor :)