web-dev-qa-db-fra.com

Éviter les problèmes avec les calculs décimaux bizarres de JavaScript

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.

33
Lèse majesté

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

22
Adriaan Stander

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

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.

Mise à jour 2017

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:

Voir également

Une autre réponse pertinente avec quelques exemples sur la façon de gérer les calculs:

31
rsp
(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

9
Bernardo Martinez

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.

5
Chris Williams

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;
}
3
Yochai Timmer

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;
}
3
Eric Seastrand