Получить доступ

Как хранить денежные суммы в коде и в базе?

JuniorЧисла в компьютере
Ещё спрашивают так:
  • Почему нельзя хранить деньги в double?
  • Чем BigDecimal лучше double для денег?
  • Как делить сумму на части без потери копейки?
Коротко

Деньги — в целых минимальных единицах (копейках) или в десятичном типе: BigDecimal, Decimal, NUMERIC в базе. Не в double.

  • double не представляет 0.10 точно — ошибки копятся при суммах
  • Явно задавать масштаб и режим округления (HALF_EVEN, HALF_UP)
  • Хранить валюту рядом с суммой

А ещё Деление на части — с распределением остатка, чтобы сошлись копейки

Так подсказку видит пользователь sotto на живом собеседовании — за пару секунд, пока вы думаете вслух.

Подробно

Двоичная плавающая точка не может точно записать 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 с округлением при выводе «достаточно».

Связанные вопросы

Закрытая бета

Подсказки на собеседовании — в реальном времени

sotto слышит вопрос интервьюера и показывает короткий ответ и план, как в блоке «Коротко». Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем