Ошибки с плавающей запятой в 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;
// ....
Умножение
Это происходит:
- в 17% операций, когда вы выполняете [целое число] * [с плавающей запятой];
- в 36% случаев, когда вы делаете [с плавающей запятой] * [с плавающей запятой];
- в 0%, когда вы делаете [целое число] * [целое число] (это ожидаемо).
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;
Деление
Как часто это происходит?
- в 16,4% операций, когда вы выполняете [с плавающей запятой]/[целое число];
- в 26% операций, когда вы выполняете [целое число]/[с плавающей запятой];
- в 36% случаев, когда вы делаете [с плавающей запятой]/[с плавающей запятой];
- в 0%, когда вы делаете целое/целое число.
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