eldinosaur's picture
PocketAccountant: custom ledger UI + deterministic agent (engine, ledger, retrieval, classifier)
c55ab5e verified
Raw
History Blame Contribute Delete
1.24 kB
"""Money helpers.
Every monetary value in the engine is a :class:`decimal.Decimal`. We never use
``float`` for money — floating point cannot represent cents exactly and tax
authorities care about the cent. All public engine functions accept ``int``,
``str`` or ``Decimal`` and return ``Decimal`` rounded to two places using
banker-free, half-up rounding (the convention the SAT and IRS expect).
"""
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from typing import Union
Number = Union[int, str, float, Decimal]
CENTS = Decimal("0.01")
def D(value: Number) -> Decimal:
"""Coerce any supported number into an *unrounded* ``Decimal``.
``float`` is routed through ``str`` so that ``0.1`` becomes ``Decimal("0.1")``
rather than the binary-approximation noise ``Decimal("0.1000000000000000055…")``.
"""
if isinstance(value, Decimal):
return value
return Decimal(str(value))
def money(value: Number) -> Decimal:
"""Coerce and round to two decimal places (half-up)."""
return D(value).quantize(CENTS, rounding=ROUND_HALF_UP)
def pct(value: Number) -> Decimal:
"""Interpret a human percentage (``16`` → ``0.16``)."""
return D(value) / Decimal(100)