Site logo

Developer Blog

Pavel Koltyshev

Ошибки с плавающей запятой в JavaScript

Содержание

Когда вы изучаете JavaScript, вас предупреждают, что 0.1 + 0.2 !== 0.3, и все это знают, верно? Это равно 0.30000000000004, и это не ошибка JavaScript, проблема в том как компьютеры представляют числа с плавающей точкой. Поэтому переписывание программы на Java или любой другой язык не поможет.

Числа с плавающей точкой представлены в двоичной системе счисления, и, иногда, точные десятичные значения не могут быть абсолютно точно представлены в этой системе. 0.1 и 0.2, в десятичной системе, представляются в двоичной системе в виде бесконечной периодической дроби. Когда вы складываете их в компьютере, результат может быть округлен до определенного конечного значения. В итоге, внутреннее представление 0.1 + 0.2 может быть чуть-чуть больше или меньше 0.3 из-за ограниченной точности представления чисел с плавающей точкой. Это приводит к тому, что операция сравнения (===) может вернуть false в некоторых случаях.

Но знаете ли вы, как часто это происходит? Я решил проверить и провел эксперимент: я создал скрипт, который перебирал все числа от 0.01 до 100.00 и выполнял операции -, +, * и /.

Сложение

В 22% операций сложения возникает эта ошибка.

5.33 + 5.2 === 10.530000000000001;
7.84 + 4.28 === 12.120000000000001;
11.92 + 207.85 === 219.76999999999998;
99.52 + 6.27 === 105.78999999999999;
939.92 + 153.49 === 1093.4099999999999;
843.32 + 131.47 === 974.7900000000001;
36067.29 + 3920.23 === 39987.520000000004;
// ....

Вычитание

В 56,6% операций вычитания мы получаем “бонус”:

8.13 - 5.75 === 2.380000000000001;
8.93 - 4.4 === 4.529999999999999;
6.09 - 3.43 === 2.6599999999999997;
4.95 - 2.82 === 2.1300000000000003;
986.83 - 93.44 === 893.3900000000001;
119.93 - 43.35 === 76.58000000000001;
490.01 - 10.91 === 479.09999999999997;
122.72 - 6.43 === 116.28999999999999;
// ....

Умножение

Это происходит:

8.38 * 0.3 === 2.5140000000000002;
9.16 * 8.22 === 75.29520000000001;
3.37 * 3.33 === 11.222100000000001;
9.68 * 8.22 === 79.56960000000001;
89.86 * 9.46 === 850.0756000000001;
73.85 * 7.81 === 576.7684999999999;
21.39 * 1.27 === 27.165300000000002;
80.04 * 8.66 === 693.1464000000001;
71.64 * 4.64 === 332.40959999999995;

Деление

Как часто это происходит?

99.27 / 3 == 33.089999999999996 (должно быть 33.09)
57.3 / 3 == 19.099999999999998 (должно быть 19.1)
73.15 / 7 == 10.450000000000001 (должно быть 10.45)
58.2 / 3 == 19.400000000000002 (должно быть 19.4)
69.96 / 3 == 23.319999999999997 (должно быть 23.32)
32.76 / 9 == 3.6399999999999997 (должно быть 3.64)
28.62 / 3 == 9.540000000000001 (должно быть 9.54)

parseFloat

parseFloat никогда не допускает ошибок. Он никогда не возвращает 54.5999999999994, если вы передаете ему строку 54.6. Он вернет ровно 54.6.

parseFloat('54.6'); // 54.6
parseFloat('123.45678901'); // 123.45678901
parseFloat('54.599999999999994'); // 54.599999999999994

Всегда ли “a + b + c” равно “c + b + a”?

Нет! Твой учитель математики говорил тебе так? Смотри:

85.13 + 5.96 + 8.44 === 99.52999999999999;
8.44 + 5.96 + 85.13 === 99.53;

94.4 + 7.12 + 4.67 === 106.19000000000001;
4.67 + 7.12 + 94.4 === 106.19;

43.57 + 5.33 + 3.05 === 51.949999999999996;
3.05 + 5.33 + 43.57 === 51.95;

Выводы

Не надейтесь, что ошибки с плавающей запятой встречаются редко. Они происходят в 17-56% всех математических операций, поэтому вероятность возникновения этой ошибки очень высока.

Когда вы пишете модульные тесты или проводите тестирование вручную, используйте цифры, которые я привел здесь.

Здесь я поделился дополнительными примерами, чтобы вы могли использовать их в своих модульных тестах: https://github.com/ellenaua/floating-point-error-examples/tree/master/examples

Как этого избежать?

Используйте библиотеки numeral.js или decimal.js для точных математических вычислений.

// decimal.js

// `0.1 + 0.2 !== 0.3`
new Decimal(0.1).add(0.2).equals(0.3); // true

// Сложение
new Decimal(1).add(2).toString(); // 3

// Вычитание
new Decimal(10).minus(4).toString(); // 6

// Умножение
new Decimal(10).mul(2).toString(); // 20

// Деление
new Decimal(50).div(3).toString(); // 16.666666666666666667

// Форматирование
new Decimal(50).div(3).toDecimalPlaces(2).toString(); // 16.67

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

Дуглас Крокфорд (Douglas Crockford) предложил решение проблемы с вещественными числами известное как DEC64: Decimal Floating Point. Но оно не вошло в стандарт JavaScript и нет предпосылок что это случиться.

На сайте https://0.30000000000000004.com можно посмотреть результат сложения чисел 0.1 + 0.2 на разных языках программирования.

Этот пост использует материалы статьи https://ellenaua.medium.com/floating-point-errors-in-javascript-node-js-21aadd897bf8