Перейти к содержанию
Innovialabs

Почему 0.1 + 0.2 ≠ 0.3: числа с плавающей точкой и деньги

Практика3 мин

Коротко

Тип float хранит числа в двоичном виде приблизительно, поэтому для денег используют целые числа в минимальных единицах (тийины, центы) или десятичные типы вроде Decimal и NUMERIC.

Короткий ответ

Большинство языков хранят дробные числа по стандарту IEEE 754 — в двоичной системе. Число 0.1 в двоичном виде бесконечная дробь, как 1/3 в десятичной. Компьютер обрезает её, и в памяти лежит очень близкое, но не точное значение.

0.1 + 0.2            // 0.30000000000000004
0.1 + 0.2 === 0.3    // false

Для научных расчётов такая погрешность обычно не важна. Для денег — важна: копейка, потерянная в одной операции, превращается в расхождение в отчёте, а сумма к оплате не совпадает с суммой в чеке.

Правило: не храните и не считайте деньги во float и double.

Как устроен float

Число в формате IEEE 754 состоит из трёх частей: знак, мантисса (значащие цифры) и экспонента (куда сдвинуть точку). Тип double даёт примерно 15–17 значащих десятичных цифр.

Отсюда два вида проблем:

  • Непредставимые дроби. 0.1, 0.2, 0.7 нельзя записать в двоичной системе точно.
  • Потеря точности на больших числах. Когда число очень велико, младшие разряды просто не помещаются в мантиссу.

Ошибки накапливаются: сложите тысячу раз по 0.1 — и результат уже заметно отличается от 100.

Способ 1: целые числа в минимальных единицах

Храните сумму как целое число в самой мелкой единице валюты: центы для доллара, тийины для сума.

  • 15 000,50 сум → 1500050 тийинов
  • $19.99 → 1999 центов

Целые числа складываются и вычитаются точно. Переводить в «человеческий» вид нужно только при выводе на экран.

price_tiyin = 1_500_050
qty = 3
total_tiyin = price_tiyin * qty          # 4 500 150, точно
print(f"{total_tiyin / 100:,.2f} сум")   # только для отображения

Учтите: у разных валют разное число знаков после запятой. Для узбекского сума по ISO 4217 их два, хотя тийины в наличном обороте почти не встречаются. Многие системы для сума ведут учёт сразу в целых сумах — это допустимо, если так зафиксировано в требованиях.

Способ 2: десятичные типы

Десятичные типы хранят число в десятичной системе и считают без двоичных погрешностей.

СредаТип
Pythondecimal.Decimal
JavaBigDecimal
C#decimal
PostgreSQL, MySQLNUMERIC(p, s) / DECIMAL(p, s)
JavaScriptсторонние библиотеки для десятичной арифметики
from decimal import Decimal, ROUND_HALF_UP

a = Decimal("0.1") + Decimal("0.2")   # Decimal('0.3')
vat = (Decimal("15000.50") * Decimal("0.12")).quantize(
    Decimal("0.01"), rounding=ROUND_HALF_UP
)

Важно создавать Decimal из строки, а не из float: Decimal(0.1) унаследует ту же погрешность.

Округление — это бизнес-правило

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

  • Режим округления. Математическое «половина вверх» или банковское (к чётному). Например, встроенный round() в Python использует банковское.
  • Момент округления. На каждой строке чека или один раз на итоге — результаты могут различаться.
  • Разделение сумм. 100 на троих не делится ровно: 33.33 + 33.33 + 33.34. Остаток явно отдают одной из частей.

Чек-лист для проекта

  1. В базе — NUMERIC или целое число в минимальных единицах, никогда FLOAT или REAL.
  2. В API — суммы строкой ("15000.50") или целым числом в минимальных единицах, рядом код валюты.
  3. Валюта хранится вместе с суммой, а не подразумевается.
  4. Правила округления описаны в требованиях и покрыты тестами.
  5. Курсы валют хранятся с достаточной точностью и датой.
  6. Фронтенд только отображает суммы, а итоговый расчёт делает сервер.

Частые ошибки

  • Сравнение float через ==.
  • parseFloat для сумм из формы с последующей арифметикой.
  • Тип money или float в JSON без договорённости о точности.
  • Округление на каждом промежуточном шаге без правила.

Частые вопросы

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

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.