Подтвердить что ты не робот

Избегайте проблем с несложными десятичными вычислениями JavaScript

I только что прочитал в MDN, что один из причуд обработки JS чисел из-за всего, что является "двухточечным 64-битным форматом IEEE 754" означает, что когда вы делаете что-то вроде .2 + .1, вы получаете 0.30000000000000004 (это то, что читает статья, но я получаю 0.29999999999999993 в Firefox). Поэтому:

(.2 + .1) * 10 == 3

имеет значение false.

Кажется, это было бы очень проблематично. Итак, что можно сделать, чтобы избежать ошибок из-за неточности десятичных вычислений в JS?

Я заметил, что если вы сделаете 1.2 + 1.1, вы получите правильный ответ. Так что вы должны избегать любой математики, которая включает в себя значения меньше 1? Потому что это кажется очень непрактичным. Существуют ли какие-либо другие опасности для математики в JS?

Edit:
Я понимаю, что многие десятичные дроби не могут быть сохранены как двоичные, но способ, которым сталкиваются большинство других языков, с которыми я столкнулся, связан с ошибкой (например, JS обрабатывает числа больше 1), кажется более интуитивным, поэтому я не привык к это, поэтому я хочу посмотреть, как другие программисты справляются с этими расчетами.

4b9b3361

Ответ 1

В подобных ситуациях вы, скорее всего, предпочтете использовать эпсилонную оценку.

Что-то вроде (псевдокод)

if (abs(((.2 + .1) * 10) - 3) > epsilon)

где epsilon - это что-то вроде 0.00000001 или любая требуемая точность.

Быстро прочитайте Сравнение чисел с плавающей запятой

Ответ 2

1.2 + 1.1 может быть нормально, но 0,2 + 0,1 может быть нехорошим.

Это проблема практически на всех языках, которые используются сегодня. Проблема в том, что 1/10 не может быть точно представлена ​​как двоичная дробь, так же как 1/3 не может быть представлена ​​как десятичная дробь.

Обходные пути включают округление только к числу десятичных знаков, которые вам нужны, и работайте со строками, которые точны:

(0.2 + 0.1).toFixed(4) === 0.3.toFixed(4) // true

или вы можете преобразовать его в числа после этого:

+(0.2 + 0.1).toFixed(4) === 0.3 // true

или используя Math.round:

Math.round(0.2 * X + 0.1 * X) / X === 0.3 // true

где X - некоторая степень 10, например. 100 или 10000 - в зависимости от того, какую точность вам нужно.

Или вы можете использовать центы вместо долларов при подсчете денег:

cents = 1499; // $14.99

Таким образом, вы работаете только с целыми числами, и вам не нужно беспокоиться о десятичной и двоичной дробях вообще.

2017 Обновление

Ситуация с представлением чисел в JavaScript может быть немного сложнее, чем раньше. Раньше у нас был только один числовой тип в JavaScript:

Это уже не так. Подробнее см. В этом ответе:

См. также

Другой важный ответ с некоторыми примерами того, как обрабатывать вычисления:

Ответ 3

(Math.floor(( 0.1+0.2 )*1000))/1000

Это уменьшит точность чисел с плавающей запятой, но решает проблему, если вы не работаете с очень маленькими значениями. Например:

.1+.2 =
0.30000000000000004

после предлагаемой операции вы получите 0.3. Но любое значение между:

0.30000000000000000
0.30000000000000999

также будет считаться 0,3

Ответ 4

Понимание ошибок округления в арифметике с плавающей запятой не для слабонервных! В принципе, вычисления выполняются так, как если бы существовали бесконечные бит точности. Затем результат округляется в соответствии с правилами, изложенными в соответствующих спецификациях IEEE.

Это округление может вызвать некоторые напуганные ответы:

Math.floor(Math.log(1000000000) / Math.LN10) == 8 // true

Это полный порядок величины. Это ошибка округления!

Для любой архитектуры с плавающей точкой существует число, представляющее наименьший интервал между различимыми числами. Он называется EPSILON.

Он будет частью стандарта EcmaScript в ближайшем будущем. Тем временем вы можете рассчитать его следующим образом:

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;
}

Затем вы можете использовать его, например:

function numericallyEquivalent(n, m) {
    var delta = Math.abs(n - m);
    return (delta < epsilon());
}

Или, поскольку ошибки округления могут накапливаться тревожно, вы можете использовать delta / 2 или delta * delta, а не delta.

Ответ 5

Вам нужно немного контроля ошибок.

Сделайте двойной метод сравнения:

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;
}

Ответ 6

Существуют библиотеки, которые стремятся решить эту проблему, но если вы не хотите включать один из них (или не можете по какой-то причине, как работа внутри переменная GTM), тогда вы можете использовать эту небольшую функцию, которую я написал:

Использование:

var a = 194.1193;
var b = 159;
a - b; // returns 35.11930000000001
doDecimalSafeMath(a, '-', b); // returns 35.1193

Здесь функция:

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;
}