PocketAccountant: custom ledger UI + deterministic agent (engine, ledger, retrieval, classifier)
c55ab5e verified | """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) | |