reports: Balance has built-in tolerance for zero comparisons.

This commit is contained in:
Brett Smith 2020-06-03 22:01:07 -04:00
parent f8f57428aa
commit 4cba2b2681
2 changed files with 26 additions and 8 deletions

View file

@ -80,10 +80,17 @@ class Balance(Mapping[str, data.Amount]):
Each key is a Beancount currency string, and each value represents the Each key is a Beancount currency string, and each value represents the
balance in that currency. balance in that currency.
""" """
__slots__ = ('_currency_map',) __slots__ = ('_currency_map', 'tolerance')
TOLERANCE = Decimal('0.01')
def __init__(self, source: Iterable[data.Amount]=()) -> None: def __init__(self,
source: Iterable[data.Amount]=(),
tolerance: Optional[Decimal]=None,
) -> None:
if tolerance is None:
tolerance = self.TOLERANCE
self._currency_map = {amount.currency: amount for amount in source} self._currency_map = {amount.currency: amount for amount in source}
self.tolerance = tolerance
def _add_amount(self, def _add_amount(self,
currency_map: MutableMapping[str, data.Amount], currency_map: MutableMapping[str, data.Amount],
@ -147,19 +154,24 @@ class Balance(Mapping[str, data.Amount]):
) -> bool: ) -> bool:
return all(op_func(amt.number, operand) for amt in self.values()) return all(op_func(amt.number, operand) for amt in self.values())
@staticmethod
def within_tolerance(dec: DecimalCompat, tolerance: DecimalCompat) -> bool:
dec = cast(Decimal, dec)
return abs(dec) < tolerance
def eq_zero(self) -> bool: def eq_zero(self) -> bool:
"""Returns true if all amounts in the balance == 0.""" """Returns true if all amounts in the balance == 0, within tolerance."""
return self._all_amounts(operator.eq, 0) return self._all_amounts(self.within_tolerance, self.tolerance)
is_zero = eq_zero is_zero = eq_zero
def ge_zero(self) -> bool: def ge_zero(self) -> bool:
"""Returns true if all amounts in the balance >= 0.""" """Returns true if all amounts in the balance >= 0, within tolerance."""
return self._all_amounts(operator.ge, 0) return self._all_amounts(operator.ge, -self.tolerance)
def le_zero(self) -> bool: def le_zero(self) -> bool:
"""Returns true if all amounts in the balance <= 0.""" """Returns true if all amounts in the balance <= 0, within tolerance."""
return self._all_amounts(operator.le, 0) return self._all_amounts(operator.le, self.tolerance)
def format(self, def format(self,
fmt: Optional[str]='#,#00.00 ¤¤', fmt: Optional[str]='#,#00.00 ¤¤',

View file

@ -92,6 +92,8 @@ def test_mixed_balance():
({'JPY': 10}, False), ({'JPY': 10}, False),
({'JPY': 10, 'BRL': 0}, False), ({'JPY': 10, 'BRL': 0}, False),
({'JPY': 10, 'BRL': 20}, False), ({'JPY': 10, 'BRL': 20}, False),
({'USD': '0.00015'}, True),
({'EUR': '-0.00052'}, True),
]) ])
def test_eq_zero(mapping, expected): def test_eq_zero(mapping, expected):
balance = core.Balance(amounts_from_map(mapping)) balance = core.Balance(amounts_from_map(mapping))
@ -108,6 +110,8 @@ def test_eq_zero(mapping, expected):
({'JPY': 10}, True), ({'JPY': 10}, True),
({'JPY': 10, 'BRL': 0}, True), ({'JPY': 10, 'BRL': 0}, True),
({'JPY': 10, 'BRL': 20}, True), ({'JPY': 10, 'BRL': 20}, True),
({'USD': '0.00015'}, True),
({'EUR': '-0.00052'}, True),
]) ])
def test_ge_zero(mapping, expected): def test_ge_zero(mapping, expected):
balance = core.Balance(amounts_from_map(mapping)) balance = core.Balance(amounts_from_map(mapping))
@ -123,6 +127,8 @@ def test_ge_zero(mapping, expected):
({'JPY': 10}, False), ({'JPY': 10}, False),
({'JPY': 10, 'BRL': 0}, False), ({'JPY': 10, 'BRL': 0}, False),
({'JPY': 10, 'BRL': 20}, False), ({'JPY': 10, 'BRL': 20}, False),
({'USD': '0.00015'}, True),
({'EUR': '-0.00052'}, True),
]) ])
def test_le_zero(mapping, expected): def test_le_zero(mapping, expected):
balance = core.Balance(amounts_from_map(mapping)) balance = core.Balance(amounts_from_map(mapping))