💸 Деньги во float — и почему баланс однажды не сойдётся
0.1 + 0.2 в float даёт не 0.3, а 0.30000000000000004
Само по себе это ерунда, но именно на этом хвосте держится половина багов с деньгами — и вылезают они не там, где хотелось бы
Разберём, где именно и как хранить деньги, чтобы потом не искать расхождения по всему проекту
🧮 Погрешность не страшна поштучно — страшна на потоке
Одно сложение с хвостом в пятнадцатом знаке погоды не делает
Проблема в накоплении: тысячи операций — суммы, проценты, скидки, конвертации — и хвосты потихоньку копятся
В какой-то момент SUM по строкам не сходится с общим итогом на копейку
И это уже не «ну почти», а расхождение в отчёте, к которому приходит финотдел
На копейку в деньгах ответ «это округление float» не принимается
🛠 Как хранить нормально
Два рабочих варианта, оба правильные
Первый — целые в минимальных единицах
Хранишь не 19.99, а 1999 копеек, целым числом
Целые складываются и вычитаются идеально точно, никаких двоичных хвостов в принципе
На показ юзеру делишь на сотню
Просто и надёжно, так живёт большинство платёжек.
Второй — специальный денежный тип, который считает в десятичной системе, а не в двоичной
В базах это NUMERIC (он же DECIMAL), в языках — готовые классы: BigDecimal в Java, Decimal в C#, тип Decimal из модуля decimal в Python
Внутри он хранит число как есть, по десятичным разрядам, поэтому 0.1 + 0.2 там честно даёт 0.3, без хвоста
Считает медленнее обычного float, но на денежных объёмах эта разница роли не играет
Что выбрать: целые копейки удобнее, когда операции простые (сложить, вычесть, показать)
Денежный тип приятнее там, где много процентов, дробей и округлений — он сам держит нужную точность и правила округления
Оба варианта рабочие, главное — не float.
➗ Целые копейки не отменяют математику
С копейками точно всё, пока не доходит до деления
Раскидай 100 рублей на троих: 33.33 + 33.33 + 33.33 = 99.99
Копейка испарилась
Обычно остаток отдают кому-то одному (первому, последнему — как договоришься), но сумма кусков обязана сойтись с целым
То же с процентами и кэшбэком: реши заранее, как округляешь — вверх, вниз или по-банковски к чётному — и держи это в одном месте
💱 Множитель ×100 — тоже ловушка
Работает, пока валюта одна
Появилась вторая — и ломается: у иены копеек нет вообще (множитель 1), у динара Бахрейна три знака (×1000)
Поэтому сумму держим в паре с валютой (amount + currency), а множитель тянем из справочника, а не хардкодим * 100 по всему проекту
Иначе на другой валюте всё может разъехаться в сто раз, и хорошо если это рано заметят
🌐 Когда отдаешь суммы - за этим тоже надо следить
Внутри всё правильно — а потом отдал наружу 19.99 числом в JSON, и на том конце парсер прочитал его обратно как float с хвостом
Гоняй между сервисами строкой ("19.99") или целыми единицами (1999) плюс валюта
Тогда на обоих концах сумма останется той же, что была
Короче: деньги — это целые копейки или decimal, всегда с валютой рядом, и точность нельзя терять ни при делении, ни на границе API
Float оставь в покое— в кошельках ему не место.
#backend #database #dev #programming #fintech #sql
Post #1531
75