El problema con los valores de coma flotante es que están tratando de representar una cantidad infinita de valores (continuos) con una cantidad fija de bits. Entonces, naturalmente, debe haber alguna pérdida en el juego, y te van a morder algunos valores.
Cuando una computadora almacena 1.275 como un valor de coma flotante, en realidad no recordará si era 1.275 o 1.27499999999999993, o incluso 1.27500000000000002. Estos valores deberían dar resultados diferentes después de redondear a dos decimales, pero no lo harán, ya que para la computadora se ven exactamente iguales después del almacenamiento como valores de punto flotante, y no hay forma de restaurar los datos perdidos. Cualquier cálculo adicional solo acumulará tal imprecisión.
Por lo tanto, si la precisión es importante, debe evitar los valores de coma flotante desde el principio. Las opciones más simples son
- usar una biblioteca dedicada
- usar cadenas para almacenar y pasar los valores (acompañados de operaciones de cadena)
- use números enteros (por ejemplo, podría pasar la cantidad de centésimas de su valor real, por ejemplo, la cantidad en centavos en lugar de la cantidad en dólares)
Por ejemplo, cuando se usan números enteros para almacenar el número de centésimos, la función para encontrar el valor real es bastante simple:
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));
Con cadenas, necesitará redondear, pero aún es manejable:
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));
Tenga en cuenta que esta función se redondea al más cercano, se vincula desde cero , mientras que IEEE 754 recomienda redondear al más cercano, incluso como el comportamiento predeterminado para las operaciones de punto flotante. Dichas modificaciones se dejan como ejercicio para el lector :)