I il suffit de lire sur MDN que l'une des bizarreries de la gestion des nombres par JS en raison de tout ce qui est "valeurs IEEE 754 au format 64 bits double précision" est que lorsque vous faites quelque chose comme .2 + .1
vous obtenez 0.30000000000000004
(c'est ce que l'article lit, mais je reçois 0.29999999999999993
dans Firefox). Par conséquent:
(.2 + .1) * 10 == 3
correspond à false
.
Cela semble être très problématique. Alors, que peut-on faire pour éviter les bogues en raison des calculs décimaux imprécis dans JS?
J'ai remarqué que si vous le faites 1.2 + 1.1
vous obtenez la bonne réponse. Alors, devriez-vous simplement éviter tout type de calcul impliquant des valeurs inférieures à 1? Parce que cela semble très impraticable. Y a-t-il d'autres dangers à faire des maths dans JS?
Modifier:
Je comprends que de nombreuses fractions décimales ne peuvent pas être stockées en binaire, mais la façon dont la plupart des autres langues que j'ai rencontrées semblent traiter l'erreur (comme JS gère les nombres supérieurs à 1) semble plus intuitive, donc je ne suis pas habitué à cela, c'est pourquoi je veux voir comment les autres programmeurs gèrent ces calculs.
Dans de telles situations, vous préférez généralement utiliser une estimation epsilon.
Quelque chose comme (pseudo code)
if (abs(((.2 + .1) * 10) - 3) > epsilon)
où epsilon est quelque chose comme 0,00000001, ou quelle que soit la précision dont vous avez besoin.
Lisez rapidement à Comparaison des nombres à virgule flottante
1,2 + 1,1 peut être correct mais 0,2 + 0,1 peut ne pas être correct.
Il s'agit d'un problème dans pratiquement toutes les langues utilisées aujourd'hui. Le problème est que 1/10 ne peut pas être représenté avec précision comme une fraction binaire comme 1/3 ne peut pas être représenté comme une fraction décimale.
Les solutions de contournement incluent l'arrondi uniquement au nombre de décimales dont vous avez besoin et fonctionnent avec des chaînes, qui sont précises:
(0.2 + 0.1).toFixed(4) === 0.3.toFixed(4) // true
ou vous pouvez ensuite le convertir en nombres:
+(0.2 + 0.1).toFixed(4) === 0.3 // true
ou en utilisant Math.round:
Math.round(0.2 * X + 0.1 * X) / X === 0.3 // true
où X
est une puissance de 10, par exemple 100 ou 10000 - selon la précision dont vous avez besoin.
Ou vous pouvez utiliser des cents au lieu de dollars pour compter l'argent:
cents = 1499; // $14.99
De cette façon, vous ne travaillez qu'avec des entiers et vous n'avez pas du tout à vous soucier des fractions décimales et binaires.
La situation de la représentation des nombres en JavaScript peut être un peu plus compliquée qu'auparavant. Il était le cas que nous n'avions qu'un seul type numérique en JavaScript:
Ce n'est plus le cas - non seulement il existe actuellement plus de types numériques dans JavaScript, mais d'autres sont en cours, y compris une proposition d'ajouter arbitraire- des entiers de précision à ECMAScript et, espérons-le, des décimales de précision arbitraire suivront - voir cette réponse pour plus de détails:
Une autre réponse pertinente avec quelques exemples sur la façon de gérer les calculs:
(Math.floor(( 0.1+0.2 )*1000))/1000
Cela réduira la précision des nombres flottants mais résout le problème si vous ne travaillez pas avec de très petites valeurs. Par exemple:
.1+.2 =
0.30000000000000004
après l'opération proposée, vous obtiendrez 0,3 Mais toute valeur entre:
0.30000000000000000
0.30000000000000999
sera également considéré comme 0,3
Comprendre les erreurs d'arrondi dans l'arithmétique à virgule flottante n'est pas pour les timides! Fondamentalement, les calculs sont effectués comme s'il y avait une infinité de bits de précision disponibles. Le résultat est ensuite arrondi selon les règles établies dans les spécifications IEEE pertinentes.
Cet arrondi peut apporter des réponses géniales:
Math.floor(Math.log(1000000000) / Math.LN10) == 8 // true
C'est un n ordre de grandeur entier out. C'est une erreur d'arrondi!
Pour toute architecture à virgule flottante, il existe un nombre qui représente le plus petit intervalle entre les nombres reconnaissables. Il s'appelle EPSILON.
Il fera partie du standard EcmaScript dans un avenir proche. En attendant, vous pouvez le calculer comme suit:
function epsilon() {
if ("EPSILON" in Number) {
return Number.EPSILON;
}
var eps = 1.0;
// Halve epsilon until we can no longer distinguish
// 1 + (eps / 2) from 1
do {
eps /= 2.0;
}
while (1.0 + (eps / 2.0) != 1.0);
return eps;
}
Vous pouvez ensuite l'utiliser, quelque chose comme ceci:
function numericallyEquivalent(n, m) {
var delta = Math.abs(n - m);
return (delta < epsilon());
}
Ou, comme les erreurs d'arrondi peuvent s'accumuler de manière alarmante, vous pouvez utiliser delta / 2
ou delta * delta
plutôt que delta
.
Vous avez besoin d'un peu de contrôle d'erreur.
Faites une petite méthode de comparaison double:
int CompareDouble(Double a,Double b) {
Double eplsilon = 0.00000001; //maximum error allowed
if ((a < b + epsilon) && (a > b - epsilon)) {
return 0;
}
else if (a < b + epsilon)
return -1;
}
else return 1;
}
Il y a bibliothèques qui cherchent à résoudre ce problème mais si vous ne voulez pas en inclure un (ou ne pouvez pas pour une raison quelconque, comme travailler à l'intérieur d'un variable GTM ) alors vous pouvez utiliser cette petite fonction que j'ai écrite:
Usage:
var a = 194.1193;
var b = 159;
a - b; // returns 35.11930000000001
doDecimalSafeMath(a, '-', b); // returns 35.1193
Voici la fonction:
function doDecimalSafeMath(a, operation, b, precision) {
function decimalLength(numStr) {
var pieces = numStr.toString().split(".");
if(!pieces[1]) return 0;
return pieces[1].length;
}
// Figure out what we need to multiply by to make everything a whole number
precision = precision || Math.pow(10, Math.max(decimalLength(a), decimalLength(b)));
a = a*precision;
b = b*precision;
// Figure out which operation to perform.
var operator;
switch(operation.toLowerCase()) {
case '-':
operator = function(a,b) { return a - b; }
break;
case '+':
operator = function(a,b) { return a + b; }
break;
case '*':
case 'x':
precision = precision*precision;
operator = function(a,b) { return a * b; }
break;
case '÷':
case '/':
precision = 1;
operator = function(a,b) { return a / b; }
break;
// Let us pass in a function to perform other operations.
default:
operator = operation;
}
var result = operator(a,b);
// Remove our multiplier to put the decimal back.
return result/precision;
}