Il problema con i valori in virgola mobile è che stanno cercando di rappresentare una quantità infinita di valori (continui) con una quantità fissa di bit. Quindi, naturalmente, ci deve essere qualche perdita in gioco e ti verrà morso con alcuni valori.
Quando un computer memorizza 1.275 come valore in virgola mobile, in realtà non ricorderà se fosse 1.275 o 1.27499999999999993, o anche 1.27500000000000002. Questi valori dovrebbero dare risultati diversi dopo l'arrotondamento a due decimali, ma non lo faranno, poiché per i computer sembrano esattamente gli stessi dopo essere stati archiviati come valori in virgola mobile e non c'è modo di ripristinare i dati persi. Ulteriori calcoli accumuleranno solo tale imprecisione.
Quindi, se la precisione conta, devi evitare i valori in virgola mobile dall'inizio. Le opzioni più semplici sono
- usare una biblioteca dedicata
- usa le stringhe per memorizzare e passare i valori (accompagnato da operazioni sulle stringhe)
- usa numeri interi (ad es. potresti passare intorno alla quantità di centesimi del tuo valore reale, ad es. importo in centesimi anziché importo in dollari)
Ad esempio, quando si utilizzano numeri interi per memorizzare il numero di centesimi, la funzione per trovare il valore effettivo è abbastanza semplice:
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 le stringhe, dovrai arrotondare, ma è comunque gestibile:
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));
Si noti che questa funzione viene arrotondata al più vicino, si lega da zero , mentre IEEE 754 consiglia di arrotondare al più vicino, si lega anche al comportamento predefinito per le operazioni in virgola mobile. Tali modifiche sono lasciate come esercizio per il lettore :)