3 # © Copyright 2021-2022, Scott Gasch
5 """An amount of money (USD) represented as an integral count of
9 from typing import Optional, Tuple
14 class CentCount(object):
15 """A class for representing monetary amounts potentially with
16 different currencies meant to avoid floating point rounding
17 issues by treating amount as a simple integral count of cents.
20 def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
21 self.strict_mode = strict_mode
22 if isinstance(centcount, str):
23 ret = CentCount._parse(centcount)
25 raise Exception(f'Unable to parse money string "{centcount}"')
28 if isinstance(centcount, float):
29 centcount = int(centcount * 100.0)
30 if not isinstance(centcount, int):
31 centcount = int(centcount)
32 self.centcount = centcount
34 self.currency: Optional[str] = None
36 self.currency = currency
39 a = float(self.centcount)
43 if self.currency is not None:
44 return f'{s} {self.currency}'
49 return CentCount(centcount=self.centcount, currency=self.currency)
52 return CentCount(centcount=-self.centcount, currency=self.currency)
54 def __add__(self, other):
55 if isinstance(other, CentCount):
56 if self.currency == other.currency:
58 centcount=self.centcount + other.centcount,
59 currency=self.currency,
62 raise TypeError('Incompatible currencies in add expression')
65 raise TypeError('In strict_mode only two moneys can be added')
67 return self.__add__(CentCount(other, self.currency))
69 def __sub__(self, other):
70 if isinstance(other, CentCount):
71 if self.currency == other.currency:
73 centcount=self.centcount - other.centcount,
74 currency=self.currency,
77 raise TypeError('Incompatible currencies in add expression')
80 raise TypeError('In strict_mode only two moneys can be added')
82 return self.__sub__(CentCount(other, self.currency))
84 def __mul__(self, other):
85 if isinstance(other, CentCount):
86 raise TypeError('can not multiply monetary quantities')
89 centcount=int(self.centcount * float(other)),
90 currency=self.currency,
93 def __truediv__(self, other):
94 if isinstance(other, CentCount):
95 raise TypeError('can not divide monetary quantities')
98 centcount=int(float(self.centcount) / float(other)),
99 currency=self.currency,
103 return self.centcount.__int__()
106 return self.centcount.__float__() / 100.0
108 def truncate_fractional_cents(self):
110 self.centcount = int(math_utils.truncate_float(x))
111 return self.centcount
113 def round_fractional_cents(self):
115 self.centcount = int(round(x, 2))
116 return self.centcount
120 def __rsub__(self, other):
121 if isinstance(other, CentCount):
122 if self.currency == other.currency:
124 centcount=other.centcount - self.centcount,
125 currency=self.currency,
128 raise TypeError('Incompatible currencies in sub expression')
131 raise TypeError('In strict_mode only two moneys can be added')
134 centcount=int(other) - self.centcount,
135 currency=self.currency,
141 # Override comparison operators to also compare currency.
143 def __eq__(self, other):
146 if isinstance(other, CentCount):
147 return self.centcount == other.centcount and self.currency == other.currency
149 raise TypeError("In strict mode only two CentCounts can be compared")
151 return self.centcount == int(other)
153 def __ne__(self, other):
154 result = self.__eq__(other)
155 if result is NotImplemented:
159 def __lt__(self, other):
160 if isinstance(other, CentCount):
161 if self.currency == other.currency:
162 return self.centcount < other.centcount
164 raise TypeError('can not directly compare different currencies')
167 raise TypeError('In strict mode, only two CentCounts can be compated')
169 return self.centcount < int(other)
171 def __gt__(self, other):
172 if isinstance(other, CentCount):
173 if self.currency == other.currency:
174 return self.centcount > other.centcount
176 raise TypeError('can not directly compare different currencies')
179 raise TypeError('In strict mode, only two CentCounts can be compated')
181 return self.centcount > int(other)
183 def __le__(self, other):
184 return self < other or self == other
186 def __ge__(self, other):
187 return self > other or self == other
189 def __hash__(self) -> int:
190 return hash(self.__repr__)
192 CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
193 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
196 def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
200 chunks = s.split(' ')
203 if CentCount.CENTCOUNT_RE.match(chunk) is not None:
204 centcount = int(float(chunk) * 100.0)
205 elif CentCount.CURRENCY_RE.match(chunk) is not None:
209 if centcount is not None and currency is not None:
210 return (centcount, currency)
211 elif centcount is not None:
212 return (centcount, 'USD')
216 def parse(cls, s: str) -> 'CentCount':
217 chunks = CentCount._parse(s)
218 if chunks is not None:
219 return CentCount(chunks[0], chunks[1])
220 raise Exception(f'Unable to parse money string "{s}"')