Le problème avec les valeurs à virgule flottante est qu'elles essaient de représenter une quantité infinie de valeurs (continues) avec une quantité fixe de bits. Donc, naturellement, il doit y avoir une perte de jeu et vous allez être mordu avec certaines valeurs.
Lorsqu'un ordinateur stocke 1.275 en tant que valeur à virgule flottante, il ne se souvient pas réellement s'il s'agissait de 1.275 ou 1.27499999999999993, ou même 1.27500000000000002. Ces valeurs devraient donner des résultats différents après avoir arrondi à deux décimales, mais elles ne le seront pas, car pour l'ordinateur, elles ont exactement la même apparence après avoir été stockées sous forme de valeurs à virgule flottante, et il n'y a aucun moyen de restaurer les données perdues. Tout calcul supplémentaire ne fera qu'accumuler une telle imprécision.
Donc, si la précision est importante, vous devez éviter les valeurs à virgule flottante dès le départ. Les options les plus simples consistent à
- utiliser une bibliothèque dédiée
- utiliser des chaînes pour stocker et transmettre les valeurs (accompagnées d'opérations sur les chaînes)
- utilisez des entiers (par exemple, vous pourriez faire circuler le montant des centièmes de votre valeur réelle, par exemple le montant en cents au lieu du montant en dollars)
Par exemple, lorsque vous utilisez des entiers pour stocker le nombre de centièmes, la fonction de recherche de la valeur réelle est assez 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));
Avec les chaînes, vous aurez besoin d'arrondir, mais c'est toujours gérable:
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));
Notez que cette fonction arrondit au plus proche, les liens loin de zéro , tandis que IEEE 754 recommande d'arrondir au plus proche, les liens à même comme comportement par défaut pour les opérations en virgule flottante. De telles modifications sont laissées en exercice au lecteur :)