Короткий ответ: потому что двоичный!

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

Вы, наверное, знаете это, но в 8 битах у вас было бы:

+=====+======+======+=====+=====+=====+=====+=================+
| 64  |  32  |  16  |  8  |  4  |  2  |  1  |  Number base 10 |
+=====+======+======+=====+=====+=====+=====+=================+
|  0  |   0  |   0  |  0  |  1  |  1  |  1  |               7 |
+-----+------+------+-----+-----+-----+-----+-----------------+
|  0  |   0  |   1  |  0  |  1  |  0  |  1  |              21 |
+-----+------+------+-----+-----+-----+-----+-----------------+

Но с дробями…

+==========================================+
| 2 | 1 | . | 1/2 | 1/4 | 1/8 | 1/16 | ... |
+==========================================+

Плавающую точку вы на самом деле не «храните», вы сообщаете компьютеру, сколько битов нужно для целочисленной части (в данном случае 2) и сколько для дробной части (в данном случае 4).

Но поскольку у вас есть все более и более мелкие части, некоторые числа не являются прямыми представлениями, и вам нужно бесконечное (или близкое к этому) количество битов, которые при добавлении дадут желаемое число.

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

Это будет очень близко, но не идеально.

Почему бы тогда не использовать основание 10?

Потому что бинарный!

Даже если вы можете сохранить его в базе 10 (скажем, со строками), вам придется выполнять операции, как в базе 10, но с базой 2 в качестве посредника… и это снизит производительность.

Опять же, должны быть другие способы сделать это, о которых я понятия не имею.

Маленькое упражнение

Как бы вы просуммировали два числа по основанию 10? (или как вы делаете это с ручкой и бумагой?)

  • проверьте знаки (плюс, добавленный к минусу, — это вычитание, а два минуса — это сложение)
  • проверить на дроби
  • «выравнивать» числа
  • суммировать каждое число
  • если одно число переполняется (то есть больше 10), то переносишь на следующее
  • вернитесь и убедитесь, что вы также добавляете переносимые номера
  • распечатайте результат!

Все это просто для суммы… это самое простое!

Введите @noriller/ручной-калькулятор

Я хотел «простой» проект, просто чтобы отвлечься… и разве вы не знаете… это намного сложнее, чем я ожидал.

https://www.npmjs.com/package/@noriller/manual-calculator

Сумма, вычитание, умножение и деление. Все, что есть.

Действительно сложным является деление, даже сейчас, в бесконечном делении, чем больше цифр вы хотите, тем больше рост будет экспоненциальным (50 тысяч цифр заняло почти 18 минут, а 1000 цифр заняло «всего» около 400 мс).

Проверьте несколько прогонов, которые я сделал здесь: https://github.com/Noriller/manual-calculation/tree/master/performance/runs

И/или попробуйте себя с этими тестами: https://github.com/Noriller/manual-calculation/tree/master/performance/tests (следуйте README, чтобы узнать, как это сделать!)

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

Итак, это было мое беспардонное продвижение…

Проверьте это и, возможно, забудете… пока вы не обнаружите, что действительно хотите это 0.1 + 0.2 === 0.3 или не захотите использовать числа, которые не будут переноситься и округляться до странных чисел, например, 987654321987654319876 становится 987654321987654300000 или даже 987654321987654254592n (с оболочкой BigInt), и, конечно же, если вам нужно деление с тысячами цифр (хотя и с некоторыми временными затратами).

Фото на обложке Michal Matlon на Unsplash