3 """An amount of money (USD) represented as an integral count of
7 from typing import Optional, Tuple
12 class CentCount(object):
13 """A class for representing monetary amounts potentially with
14 different currencies meant to avoid floating point rounding
15 issues by treating amount as a simple integral count of cents.
18 def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
19 self.strict_mode = strict_mode
20 if isinstance(centcount, str):
21 ret = CentCount._parse(centcount)
23 raise Exception(f'Unable to parse money string "{centcount}"')
26 if isinstance(centcount, float):
27 centcount = int(centcount * 100.0)
28 if not isinstance(centcount, int):
29 centcount = int(centcount)
30 self.centcount = centcount
32 self.currency: Optional[str] = None
34 self.currency = currency
37 a = float(self.centcount)
41 if self.currency is not None:
42 return f'{s} {self.currency}'
47 return CentCount(centcount=self.centcount, currency=self.currency)
50 return CentCount(centcount=-self.centcount, currency=self.currency)
52 def __add__(self, other):
53 if isinstance(other, CentCount):
54 if self.currency == other.currency:
56 centcount=self.centcount + other.centcount,
57 currency=self.currency,
60 raise TypeError('Incompatible currencies in add expression')
63 raise TypeError('In strict_mode only two moneys can be added')
65 return self.__add__(CentCount(other, self.currency))
67 def __sub__(self, other):
68 if isinstance(other, CentCount):
69 if self.currency == other.currency:
71 centcount=self.centcount - other.centcount,
72 currency=self.currency,
75 raise TypeError('Incompatible currencies in add expression')
78 raise TypeError('In strict_mode only two moneys can be added')
80 return self.__sub__(CentCount(other, self.currency))
82 def __mul__(self, other):
83 if isinstance(other, CentCount):
84 raise TypeError('can not multiply monetary quantities')
87 centcount=int(self.centcount * float(other)),
88 currency=self.currency,
91 def __truediv__(self, other):
92 if isinstance(other, CentCount):
93 raise TypeError('can not divide monetary quantities')
96 centcount=int(float(self.centcount) / float(other)),
97 currency=self.currency,
101 return self.centcount.__int__()
104 return self.centcount.__float__() / 100.0
106 def truncate_fractional_cents(self):
108 self.centcount = int(math_utils.truncate_float(x))
109 return self.centcount
111 def round_fractional_cents(self):
113 self.centcount = int(round(x, 2))
114 return self.centcount
118 def __rsub__(self, other):
119 if isinstance(other, CentCount):
120 if self.currency == other.currency:
122 centcount=other.centcount - self.centcount,
123 currency=self.currency,
126 raise TypeError('Incompatible currencies in sub expression')
129 raise TypeError('In strict_mode only two moneys can be added')
132 centcount=int(other) - self.centcount,
133 currency=self.currency,
139 # Override comparison operators to also compare currency.
141 def __eq__(self, other):
144 if isinstance(other, CentCount):
145 return self.centcount == other.centcount and self.currency == other.currency
147 raise TypeError("In strict mode only two CentCounts can be compared")
149 return self.centcount == int(other)
151 def __ne__(self, other):
152 result = self.__eq__(other)
153 if result is NotImplemented:
157 def __lt__(self, other):
158 if isinstance(other, CentCount):
159 if self.currency == other.currency:
160 return self.centcount < other.centcount
162 raise TypeError('can not directly compare different currencies')
165 raise TypeError('In strict mode, only two CentCounts can be compated')
167 return self.centcount < int(other)
169 def __gt__(self, other):
170 if isinstance(other, CentCount):
171 if self.currency == other.currency:
172 return self.centcount > other.centcount
174 raise TypeError('can not directly compare different currencies')
177 raise TypeError('In strict mode, only two CentCounts can be compated')
179 return self.centcount > int(other)
181 def __le__(self, other):
182 return self < other or self == other
184 def __ge__(self, other):
185 return self > other or self == other
187 def __hash__(self) -> int:
188 return hash(self.__repr__)
190 CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
191 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
194 def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
198 chunks = s.split(' ')
201 if CentCount.CENTCOUNT_RE.match(chunk) is not None:
202 centcount = int(float(chunk) * 100.0)
203 elif CentCount.CURRENCY_RE.match(chunk) is not None:
207 if centcount is not None and currency is not None:
208 return (centcount, currency)
209 elif centcount is not None:
210 return (centcount, 'USD')
214 def parse(cls, s: str) -> 'CentCount':
215 chunks = CentCount._parse(s)
216 if chunks is not None:
217 return CentCount(chunks[0], chunks[1])
218 raise Exception(f'Unable to parse money string "{s}"')