Вопрос с собеседования · Основы

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

JuniorЧисла в компьютере
Ещё спрашивают так:
  • Почему нельзя хранить деньги в 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 минут бесплатно.

Разбор собеседования