Прямой ответ
Денежное значение хранится в точном типе вместе с кодом валюты, поэтому сумма не теряет разряды при арифметике и не смешивается между валютами.
| Параметр | Что определяет |
|---|---|
| Тип значения | Точность хранения суммы |
| Код валюты | Валюту, к которой относится сумма |
| Единица хранения | Минимальная единица либо десятичная дробь |
| Округление | Правило приведения результата к разряду |
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. Проверяйте итог по контрольной сумме. Расхождение указывает на погрешность типа либо на неверное правило округления.
Рекомендации:
- Не храните деньги в двоичном типе с плавающей точкой. Часть десятичных дробей не представляется точно, поэтому итог расходится.
- Учитывайте зависимость минимальной единицы от валюты. Число разменных единиц в основной единице различается между валютами.
- Сопоставляйте точность хранения с разрядностью валюты. Разрядность в схеме должна покрывать минимальную единицу валюты.
- Фиксируйте порядок округления в расчёте. Округление на каждом шаге даёт иной итог, чем округление после расчёта.
- Пересматриваем хранение при добавлении валюты. Новая валюта может требовать иной разрядности минимальной единицы.
Практический сценарий: итог заказа расходился на копейки
Контекст: сумма заказа в приложении отличалась от суммы в банковской выписке на несколько копеек, при этом состав заказа совпадал.
- Диагностика. Проверка показала, что цена позиции хранилась в типе с плавающей точкой. Умножение цены на количество давало значение с погрешностью в последних разрядах, а сложение позиций накапливало расхождение.
- Причина. Двоичное представление не хранит часть десятичных дробей точно, поэтому арифметика вносила погрешность, растущую с числом позиций.
- Действия. (а) Заменили хранение цены и суммы на десятичный тип с фиксированной точностью, чтобы значение представлялось точно; (б) добавили код валюты к каждому денежному значению, чтобы исключалось смешение сумм; (в) перенесли округление в конец расчёта, чтобы промежуточные значения сохраняли точность; (г) пересчитали исторические заказы и сравнили с выпиской; (д) добавили проверку контрольной суммы заказа, чтобы расхождение обнаруживалось.
- Результат. Сумма заказа совпадает с банковской выпиской, поскольку арифметика выполняется без погрешности представления.
Вывод: хранение денежных значений в двоичном типе с плавающей точкой вносит погрешность, которая накапливается при арифметике и приводит к расхождению итога. Десяточный тип с фиксированной точностью и округление после полного расчёта устраняют расхождение.
FAQ
Почему нельзя хранить деньги в float? Двоичное представление не хранит часть десятичных дробей точно, поэтому арифметика вносит погрешность, накапливающуюся при последовательных операциях.
Зачем хранить код валюты вместе с суммой? Сумма без кода валюты складывается с суммой в другой валюте, поэтому результат не имеет смысла, а ошибка не обнаруживается.
Когда выполнять округление денежного значения? После полного вычисления. Округление на каждом шаге даёт итог, отличающийся от округления после расчёта.
Что лучше: минимальные единицы или десятичный тип? Оба способа дают точное представление. Минимальные единицы требуют явного указания валюты, поскольку разрядность зависит от валюты.
Источники: Python decimal, PostgreSQL numeric types, PostgreSQL money type — ссылки в шапке.