food-records/app/money.py

69 lines
2.7 KiB
Python
Raw Normal View History

"""Деньги и количества.
Всё считаем в Decimal, а не в float: 0.1 + 0.2 в float даёт 0.30000000000004,
и такие копейки накапливаются в отчётах по партиям. В JSON кладём строкой,
чтобы точность не терялась при сохранении.
"""
from __future__ import annotations
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
CENTS = Decimal("0.01")
QTY_STEP = Decimal("0.001")
ZERO = Decimal("0")
def money(value) -> Decimal:
"""Привести что угодно к сумме денег, округлив до копеек."""
return _to_decimal(value).quantize(CENTS, rounding=ROUND_HALF_UP)
def qty(value) -> Decimal:
"""Привести что угодно к количеству.
До тысячных булки штучные, но так же работают вес и объём.
"""
return _to_decimal(value).quantize(QTY_STEP, rounding=ROUND_HALF_UP)
def _to_decimal(value) -> Decimal:
if isinstance(value, Decimal):
return value
if value is None or value == "":
return ZERO
if isinstance(value, float):
# Через str, иначе Decimal(0.1) даст 0.1000000000000000055511151231257827.
value = repr(value)
try:
return Decimal(str(value).replace(",", ".").replace(" ", "").replace(" ", ""))
except (InvalidOperation, ValueError) as exc:
raise ValueError(f"Не похоже на число: {value!r}") from exc
def dumps(value: Decimal) -> str:
"""Сериализовать сумму для JSON."""
return format(value, "f")
def fmt_money(value, currency: str = "") -> str:
"""Отформатировать сумму для показа: 12 345,67 ₽."""
value = money(value)
sign = "-" if value < 0 else ""
whole, _, frac = format(abs(value), "f").partition(".")
groups = []
while len(whole) > 3:
groups.insert(0, whole[-3:])
whole = whole[:-3]
groups.insert(0, whole)
text = f"{sign}{' '.join(groups)},{frac}"
# Разделители — обычные пробелы, а не неразрывные: то, что видно на экране,
# должно находиться поиском по журналу ровно так, как пользователь это набрал.
return f"{text} {currency}" if currency else text
def fmt_qty(value) -> str:
"""Отформатировать количество, пряча незначащие нули: 5, 2,5."""
text = format(qty(value), "f").rstrip("0").rstrip(".")
return (text or "0").replace(".", ",")