-
Notifications
You must be signed in to change notification settings - Fork 0
/
test_arithmetic.py
217 lines (176 loc) · 5.69 KB
/
test_arithmetic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
from __future__ import annotations
import enum
from collections.abc import Callable
from collections.abc import Sequence
from fractions import Fraction
from functools import reduce
from operator import add
from operator import mul
from operator import sub
from typing import Final
from typing import TypeAlias
from hypothesis import example
from hypothesis import given
from hypothesis.strategies import DrawFn
from hypothesis.strategies import SearchStrategy
from hypothesis.strategies import composite
from hypothesis.strategies import integers
from hypothesis.strategies import lists
from hypothesis.strategies import sampled_from
from immoney import Currency
from immoney import Money
from immoney import Overdraft
from immoney import SubunitFraction
from immoney.currencies import SEK
from immoney.currencies import SEKType
def _to_integer_subunit(value: Money[SEKType] | Overdraft[SEKType]) -> int:
if isinstance(value, Money):
return value.subunits
elif isinstance(value, Overdraft):
return -value.subunits
raise NotImplementedError
def _from_integer_subunit(value: int) -> Money[SEKType] | Overdraft[SEKType]:
return SEK.from_subunit(value) if value >= 0 else SEK.overdraft_from_subunit(-value)
@given(lists(integers(), min_size=1))
@example([1])
@example([0, -1])
@example([10000000000000000000000000001])
def test_sequence_of_additions(values: list[int]):
monetary_sum = sum((_from_integer_subunit(value) for value in values), SEK.zero)
int_sum = sum(values)
assert int_sum == _to_integer_subunit(monetary_sum)
@given(lists(integers(), min_size=1))
@example([1])
@example([0, -1])
@example([10000000000000000000000000001])
def test_sequence_of_subtractions(values: list[int]):
monetary_delta = reduce(
sub,
(_from_integer_subunit(value) for value in values),
)
int_delta = reduce(sub, values)
assert int_delta == _to_integer_subunit(monetary_delta)
def truediv(
a: Numeric | Monetary,
b: Numeric,
/,
) -> Fraction | SubunitFraction[Currency]:
if isinstance(a, int | Fraction):
return Fraction(a, b)
elif isinstance(a, Money | Overdraft | SubunitFraction):
return a / b
raise NotImplementedError
def rtruediv(
a: Numeric | Monetary,
b: Numeric,
/,
) -> Fraction | SubunitFraction[Currency]:
if isinstance(a, int | Fraction):
return Fraction(b, a)
elif isinstance(a, Money | Overdraft | SubunitFraction):
return b / a
raise NotImplementedError
def radd(
a: Numeric | Monetary,
b: Numeric,
/,
) -> AnyVal:
return add(b, a) # type: ignore[no-any-return]
def rsub(
a: Numeric | Monetary,
b: Numeric,
/,
) -> AnyVal:
return sub(b, a) # type: ignore[no-any-return]
def rmul(
a: Numeric | Monetary,
b: Numeric,
/,
) -> AnyVal:
return mul(b, a) # type: ignore[no-any-return]
Numeric: TypeAlias = int | Fraction
Monetary: TypeAlias = Money[Currency] | Overdraft[Currency] | SubunitFraction[Currency]
AnyVal: TypeAlias = Numeric | Monetary
Operator: TypeAlias = (
Callable[[Numeric, Numeric], Numeric]
| Callable[[AnyVal, AnyVal], AnyVal]
| Callable[[AnyVal, Numeric], Fraction | SubunitFraction[Currency]]
)
Operation: TypeAlias = tuple[Operator, Numeric]
MonetaryOperation: TypeAlias = tuple[Operator, AnyVal]
operators: Final = sub, add, mul, rmul, truediv, rtruediv
class SequenceError(enum.Enum):
zero_division = enum.auto()
def to_subunit(value: Monetary) -> Numeric:
if isinstance(value, Money):
return value.subunits
elif isinstance(value, Overdraft):
return -value.subunits
elif isinstance(value, SubunitFraction):
return value.value
raise NotImplementedError
def from_subunit(value: Numeric) -> Monetary:
if isinstance(value, Fraction):
return SubunitFraction(value, SEK)
return _from_integer_subunit(value)
@composite
def operations(
draw: DrawFn,
operands: SearchStrategy[int] = integers(),
ops: SearchStrategy[Operator] = sampled_from(operators),
) -> Operation:
operator = draw(ops)
operand = draw(operands)
return operator, operand
def apply(value: AnyVal, operation: Operation | MonetaryOperation) -> AnyVal:
operator, operand = operation
return operator(value, operand) # type: ignore[arg-type]
def monetary_operation(operation: Operation) -> MonetaryOperation:
operator, operand = operation
if operator in (sub, rsub, add, radd):
return operator, from_subunit(operand)
if operator in (mul, rmul, truediv, rtruediv):
return operation
raise NotImplementedError
@given(lists(operations()), integers())
@example(
[
(add, 16),
(sub, 1),
(radd, 18),
(rsub, 64),
(mul, 2),
(rmul, 2),
(truediv, 2),
(truediv, 2),
(rtruediv, 2),
],
1,
)
# This found a bug where rtruediv returned 0 instead of raising a zero division error.
@example([(rtruediv, 0)], 0)
def test_sequence_of_operations(
operations: Sequence[Operation],
initial: int,
) -> None:
monetary_result: int | Fraction | SequenceError
int_result: int | Fraction | SequenceError
try:
monetary = reduce(
apply, # type: ignore[arg-type]
(monetary_operation(operation) for operation in operations),
from_subunit(initial),
)
except ZeroDivisionError:
monetary_result = SequenceError.zero_division
else:
monetary_result = to_subunit(monetary)
try:
int_result = reduce(
apply, # type: ignore[arg-type]
operations,
initial,
)
except ZeroDivisionError:
int_result = SequenceError.zero_division
assert int_result == monetary_result