Как хранить денежные суммы в коде и в базе?
- Почему нельзя хранить деньги в double?
- Чем BigDecimal лучше double для денег?
- Как делить сумму на части без потери копейки?
Деньги — в целых минимальных единицах (копейках) или в десятичном типе: BigDecimal, Decimal, NUMERIC в базе. Не в double.
- double не представляет 0.10 точно — ошибки копятся при суммах
- Явно задавать масштаб и режим округления (HALF_EVEN, HALF_UP)
- Хранить валюту рядом с суммой
А ещё Деление на части — с распределением остатка, чтобы сошлись копейки
Подробно
Двоичная плавающая точка не может точно записать 0.10 или 0.01, поэтому сумма тысячи платежей по 0.10 рубля в double — не ровно 100. Для отчётов и сверок с банком такая погрешность неприемлема. Варианты:
- целые минимальные единицы — хранить копейки в
long/BIGINT. Быстро, просто, точно. Нужно знать число знаков после запятой у валюты: у рубля и доллара 2, у иены 0, у бахрейнского динара 3; - десятичный тип —
BigDecimalв Java,Decimalв Python,NUMERIC(19, 4)в PostgreSQL. Число хранится как целое и десятичный масштаб, поэтому 0.10 представимо точно. Медленнее целых, но удобнее для процентов, курсов и налогов.
Округление. Любое деление и умножение на процент даёт лишние знаки. Режим округления выбирают явно и по правилам предметной области: HALF_UP — школьное, HALF_EVEN — банковское (к чётному, не смещает сумму при большом числе операций). В Java new BigDecimal(0.1) берёт неточное значение double — создавать надо из строки или через BigDecimal.valueOf. И сравнивать через compareTo: equals учитывает масштаб, и 1.0 не равно 1.00.
Распределение. 100 рублей на троих — 33.33 × 3 = 99.99. Потерянную копейку отдают кому-то явно: считают доли, а остаток раскладывают по одной копейке первым участникам. Так делают при разбивке платежа на части и пропорциональном списании скидки по позициям чека.
Валюта. Сумма без валюты — источник ошибок. Хранят пару (сумма, код ISO 4217) или используют тип вроде javax.money/Joda-Money.
Пример
from decimal import Decimal, ROUND_HALF_EVEN
def split(total_kop: int, parts: int) -> list[int]:
base, rest = divmod(total_kop, parts)
return [base + (1 if i < rest else 0) for i in range(parts)]
split(10000, 3) # [3334, 3333, 3333] — в сумме ровно 10000
Decimal("2.675").quantize(Decimal("0.01"), ROUND_HALF_EVEN) # 2.68
round(2.675, 2) # 2.67 — double хранит 2.67499999…Что хотят услышать
- Почему не double — двоичное представление.
- Два рабочих варианта: копейки в целом или десятичный тип.
- Явный режим округления и создание BigDecimal из строки.
- Распределение остатка и хранение валюты — плюс.
Частые ошибки
new BigDecimal(0.1)и сравнение черезequals.- Считать, что
floatс округлением при выводе «достаточно».
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗В double с округлением до двух знаков при выводе: погрешность в шестнадцатом знаке на суммы не влияет
- ✓В целых копейках или десятичном типе (BigDecimal, NUMERIC) с явным округлением и валютой рядом
- ✗Валюту хранить не нужно — она всегда рубли
- ✗float точнее double для денег
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.