From 0f58f0e16f90fe1a02323256d727d38e72bce717 Mon Sep 17 00:00:00 2001 From: Anton Agestam Date: Tue, 7 Nov 2023 18:46:18 +0100 Subject: [PATCH] feature: Implement SubunitFraction ordering --- src/immoney/_base.py | 36 ++++++++++ tests/strategies.py | 23 +++++++ tests/test_subunit_fraction.py | 117 +++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) diff --git a/src/immoney/_base.py b/src/immoney/_base.py index 890ed44..481c52f 100644 --- a/src/immoney/_base.py +++ b/src/immoney/_base.py @@ -498,6 +498,42 @@ def __eq__(self, other: object) -> bool: return self.value == -other.subunits return NotImplemented + def __gt__(self, other: Self | Money[C_co] | Overdraft[C_co]) -> bool: + if isinstance(other, SubunitFraction) and self.currency == other.currency: + return self.value > other.value + if isinstance(other, Money) and self.currency == other.currency: + return self.value > other.subunits + if isinstance(other, Overdraft) and self.currency == other.currency: + return self.value > -other.subunits + return NotImplemented + + def __ge__(self, other: Self | Money[C_co] | Overdraft[C_co]) -> bool: + if isinstance(other, SubunitFraction) and self.currency == other.currency: + return self.value >= other.value + if isinstance(other, Money) and self.currency == other.currency: + return self.value >= other.subunits + if isinstance(other, Overdraft) and self.currency == other.currency: + return self.value >= -other.subunits + return NotImplemented + + def __lt__(self, other: Self | Money[C_co] | Overdraft[C_co]) -> bool: + if isinstance(other, SubunitFraction) and self.currency == other.currency: + return self.value < other.value + if isinstance(other, Money) and self.currency == other.currency: + return self.value < other.subunits + if isinstance(other, Overdraft) and self.currency == other.currency: + return self.value < -other.subunits + return NotImplemented + + def __le__(self, other: Self | Money[C_co] | Overdraft[C_co]) -> bool: + if isinstance(other, SubunitFraction) and self.currency == other.currency: + return self.value <= other.value + if isinstance(other, Money) and self.currency == other.currency: + return self.value <= other.subunits + if isinstance(other, Overdraft) and self.currency == other.currency: + return self.value <= -other.subunits + return NotImplemented + def __neg__(self) -> SubunitFraction[C_co]: return SubunitFraction(-self.value, self.currency) diff --git a/tests/strategies.py b/tests/strategies.py index 82204c1..6b1d482 100644 --- a/tests/strategies.py +++ b/tests/strategies.py @@ -2,16 +2,22 @@ import random from typing import Final +from typing import TypeAlias from hypothesis.strategies import composite from hypothesis.strategies import decimals +from hypothesis.strategies import fractions from hypothesis.strategies import integers +from hypothesis.strategies import just from hypothesis.strategies import text from immoney import Currency from immoney import Money from immoney._base import Overdraft +from immoney._base import SubunitFraction from immoney._base import valid_subunit +from immoney.currencies import SEK +from immoney.currencies import SEKType valid_sek_decimals: Final = decimals( min_value=0, @@ -53,3 +59,20 @@ def overdrafts( subunits=integers(min_value=1), ) -> Overdraft[Currency]: return Overdraft.from_subunit(draw(subunits), draw(currencies)) + + +@composite +def subunit_fractions( + draw, + currencies=currencies(), + subunits=fractions(), +) -> SubunitFraction[Currency]: + return SubunitFraction(draw(subunits), draw(currencies)) + + +sek_monetaries: Final = ( + subunit_fractions(currencies=just(SEK)) + | monies(currencies=just(SEK)) + | overdrafts(currencies=just(SEK)) +) +SEKMonetary: TypeAlias = Money[SEKType] | Overdraft[SEKType] | SubunitFraction[SEKType] diff --git a/tests/test_subunit_fraction.py b/tests/test_subunit_fraction.py index 340d008..af9022f 100644 --- a/tests/test_subunit_fraction.py +++ b/tests/test_subunit_fraction.py @@ -20,7 +20,12 @@ from immoney.currencies import SEKType from immoney.errors import ParseError +from .strategies import SEKMonetary +from .strategies import currencies from .strategies import monies +from .strategies import overdrafts +from .strategies import sek_monetaries +from .strategies import subunit_fractions def test_init_normalizes_value() -> None: @@ -124,6 +129,60 @@ def test_equality() -> None: assert different_one != -overdraft_one +@given(monies()) +def test_compares_equal_to_same_currency_money(money: Money[Currency]) -> None: + fraction = money.currency.fraction(money.subunits) + assert fraction == money + assert money == fraction + assert fraction >= money + assert fraction <= money + assert money >= fraction + assert money <= fraction + + +@given(overdrafts()) +def test_compares_equal_to_same_currency_overdraft( + overdraft: Overdraft[Currency], +) -> None: + fraction = overdraft.currency.fraction(-overdraft.subunits) + assert fraction == overdraft + assert overdraft == fraction + assert fraction >= overdraft + assert fraction <= overdraft + assert overdraft >= fraction + assert overdraft <= fraction + + +@given(monies(), currencies()) +def test_compares_unequal_to_differing_currency_money( + money: Money[Currency], + currency: Currency, +) -> None: + fraction = currency.fraction(money.subunits) + assert fraction != money + assert money != fraction + + +@given(monies(), currencies()) +def test_compares_unequal_to_differing_currency_overdraft( + overdraft: Overdraft[Currency], + currency: Currency, +) -> None: + fraction = currency.fraction(overdraft.subunits) + assert fraction != overdraft + assert overdraft != fraction + + +def test_compares_unequal_across_values() -> None: + fraction = SEK.fraction(99, 100) + money = SEK(1) + assert fraction != money + assert money != fraction + overdraft = SEK.overdraft(1) + assert fraction != overdraft + assert overdraft != fraction + + def test_from_money_returns_instance() -> None: class FooType(Currency): code = "foo" @@ -434,3 +493,61 @@ def test_raises_type_error_for_invalid_other( ), ): b / a # type: ignore[operator] + + +class TestOrdering: + @given(sek_monetaries, sek_monetaries) + def test_total_ordering_within_currency( + self, a: SEKMonetary, b: SEKMonetary + ) -> None: + assert (a > b and b < a) or (a < b and b > a) or (a == b and b == a) + assert (a >= b and b <= a) or (a <= b and b >= a) + + @pytest.mark.parametrize( + ("larger", "smaller"), + [ + (SEK.fraction(0), SEK.overdraft_from_subunit(1)), + (SEK.overdraft_from_subunit(1), SEK.fraction(-2)), + (SEK.fraction(1), SEK.zero), + (SEK.zero, SEK.fraction(-1)), + (SEK(1), SEK.fraction(1, 2)), + ], + ) + def test_all_ordering_combinations( + self, + larger: SubunitFraction[Currency] | Overdraft[Currency] | Money[Currency], + smaller: SubunitFraction[Currency] | Overdraft[Currency] | Money[Currency], + ) -> None: + assert larger > smaller + assert not larger < smaller + assert larger >= smaller + assert not larger <= smaller + assert smaller < larger + assert not smaller > larger + assert smaller <= larger + assert not smaller >= larger + + @given(a=subunit_fractions(), b=overdrafts() | monies() | subunit_fractions()) + @example(NOK.fraction(1), SEK.fraction(1)) + @example(SEK.fraction(1), NOK.fraction(2)) + def test_raises_type_error_for_ordering_across_currencies( + self, + a: SubunitFraction[Currency], + b: SubunitFraction[Currency] | Overdraft[Currency] | Money[Currency], + ) -> None: + with pytest.raises(TypeError): + a > b # noqa: B015 + with pytest.raises(TypeError): + a >= b # noqa: B015 + with pytest.raises(TypeError): + a < b # noqa: B015 + with pytest.raises(TypeError): + a <= b # noqa: B015 + with pytest.raises(TypeError): + b > a # noqa: B015 + with pytest.raises(TypeError): + b >= a # noqa: B015 + with pytest.raises(TypeError): + b < a # noqa: B015 + with pytest.raises(TypeError): + b <= a # noqa: B015