Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → Деньги в приложении: хранение и расчёт

Деньги в приложении: типы, хранение и арифметика

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

Прямой ответ

Денежное значение хранится в точном типе вместе с кодом валюты, поэтому сумма не теряет разряды при арифметике и не смешивается между валютами.

Параметр Что определяет
Тип значения Точность хранения суммы
Код валюты Валюту, к которой относится сумма
Единица хранения Минимальная единица либо десятичная дробь
Округление Правило приведения результата к разряду
from decimal import Decimal

price = Decimal("19.99")
quantity = 3
total = price * quantity          # Decimal("59.97")
currency = "RUB"

Сумма хранится в десятичном типе, поэтому умножение не даёт погрешности двоичного представления. О выборе типа — в статье про Decimal против float.

Точный тип и погрешность представления

Точный тип хранит значение без погрешности двоичного представления, поэтому арифметика денежных сумм не даёт расхождения в последних разрядах.

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

Двоичное представление. Значение в двоичном типе не представляет часть десятичных дробей точно, поэтому результат арифметики содержит погрешность.

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

Практическое следствие: сумма, посчитанная в двоичном типе по тысяче позиций, расходится с ожидаемой на несколько разменных единиц, поэтому расчёт не сходится с отчётностью.

Отдельный аспект — хранение в минимальных единицах. Сумма в копейках либо центах хранится целым числом, поэтому арифметика выполняется без дробной части. Такой способ требует явного указания валюты, поскольку минимальная единица зависит от валюты.

Код валюты и смешение сумм

Код валюты определяет, к какой валюте относится сумма, поэтому сложение сумм в разных валютах без пересчёта недопустимо.

Код валюты. Сумма сопровождается кодом валюты, поэтому значение однозначно относится к одной валюте.

Проверка при операции. Операция над суммами проверяет совпадение кодов, поэтому сложение сумм в разных валютах отклоняется.

Пересчёт по курсу. Сумма в иной валюте пересчитывается по курсу, поэтому операция выполняется над значениями в одной валюте.

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

Хранение суммы и правила арифметики

Хранение суммы и валюты определяется схемой, поэтому правило арифметики зависит от способа хранения.

Хранение в базе. Сумма хранится в числовом типе с фиксированной точностью, поэтому разрядность значения ограничена схемой.

Арифметика в приложении. Операции выполняются в точном типе, поэтому результат приводится к разряду по правилу округления.

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

Практическое следствие: округление на каждом шаге вычисления даёт итог, отличающийся от округления после полного расчёта, поэтому правило фиксируется явно.

О правиле округления — в статье про округление денежных значений; о хранении в базе — в статье про денежные операции в БД.

Практика: работа с денежными значениями

1. Храните сумму в точном типе. Арифметика не даёт погрешности двоичного представления, поэтому итог совпадает с ожидаемым.

2. Сохраняйте код валюты вместе с суммой. Значение однозначно относится к валюте, поэтому смешение сумм исключается.

3. Проверяйте совпадение валют при операции. Сложение сумм в разных валютах отклоняется вместо неявного сложения.

4. Храните сумму в минимальных единицах либо в десятичном типе. Оба способа дают точное представление разменных единиц.

5. Округляйте после полного вычисления. Промежуточные значения сохраняют точность, поэтому итог не накапливает погрешность.

6. Фиксируйте правило округления в коде. Правило применяется единообразно, поэтому расчёт воспроизводим.

7. Проверяйте итог по контрольной сумме. Расхождение указывает на погрешность типа либо на неверное правило округления.

Рекомендации:

  1. Не храните деньги в двоичном типе с плавающей точкой. Часть десятичных дробей не представляется точно, поэтому итог расходится.
  2. Учитывайте зависимость минимальной единицы от валюты. Число разменных единиц в основной единице различается между валютами.
  3. Сопоставляйте точность хранения с разрядностью валюты. Разрядность в схеме должна покрывать минимальную единицу валюты.
  4. Фиксируйте порядок округления в расчёте. Округление на каждом шаге даёт иной итог, чем округление после расчёта.
  5. Пересматриваем хранение при добавлении валюты. Новая валюта может требовать иной разрядности минимальной единицы.

Практический сценарий: итог заказа расходился на копейки

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

  1. Диагностика. Проверка показала, что цена позиции хранилась в типе с плавающей точкой. Умножение цены на количество давало значение с погрешностью в последних разрядах, а сложение позиций накапливало расхождение.
  2. Причина. Двоичное представление не хранит часть десятичных дробей точно, поэтому арифметика вносила погрешность, растущую с числом позиций.
  3. Действия. (а) Заменили хранение цены и суммы на десятичный тип с фиксированной точностью, чтобы значение представлялось точно; (б) добавили код валюты к каждому денежному значению, чтобы исключалось смешение сумм; (в) перенесли округление в конец расчёта, чтобы промежуточные значения сохраняли точность; (г) пересчитали исторические заказы и сравнили с выпиской; (д) добавили проверку контрольной суммы заказа, чтобы расхождение обнаруживалось.
  4. Результат. Сумма заказа совпадает с банковской выпиской, поскольку арифметика выполняется без погрешности представления.

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

FAQ

Почему нельзя хранить деньги в float? Двоичное представление не хранит часть десятичных дробей точно, поэтому арифметика вносит погрешность, накапливающуюся при последовательных операциях.

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

Когда выполнять округление денежного значения? После полного вычисления. Округление на каждом шаге даёт итог, отличающийся от округления после расчёта.

Что лучше: минимальные единицы или десятичный тип? Оба способа дают точное представление. Минимальные единицы требуют явного указания валюты, поскольку разрядность зависит от валюты.

Источники: Python decimal, PostgreSQL numeric types, PostgreSQL money type — ссылки в шапке.