4 from typing import Optional, Tuple, TypeVar
9 class CentCount(object):
10 """A class for representing monetary amounts potentially with
11 different currencies meant to avoid floating point rounding
12 issues by treating amount as a simple integral count of cents.
15 def __init__(self, centcount, currency: str = 'USD', *, strict_mode=False):
16 self.strict_mode = strict_mode
17 if isinstance(centcount, str):
18 ret = CentCount._parse(centcount)
20 raise Exception(f'Unable to parse money string "{centcount}"')
23 if isinstance(centcount, float):
24 centcount = int(centcount * 100.0)
25 if not isinstance(centcount, int):
26 centcount = int(centcount)
27 self.centcount = centcount
29 self.currency: Optional[str] = None
31 self.currency = currency
34 a = float(self.centcount)
38 if self.currency is not None:
39 return '%s %s' % (s, self.currency)
44 return CentCount(centcount=self.centcount, currency=self.currency)
47 return CentCount(centcount=-self.centcount, currency=self.currency)
49 def __add__(self, other):
50 if isinstance(other, CentCount):
51 if self.currency == other.currency:
53 centcount=self.centcount + other.centcount,
54 currency=self.currency,
57 raise TypeError('Incompatible currencies in add expression')
60 raise TypeError('In strict_mode only two moneys can be added')
62 return self.__add__(CentCount(other, self.currency))
64 def __sub__(self, other):
65 if isinstance(other, CentCount):
66 if self.currency == other.currency:
68 centcount=self.centcount - other.centcount,
69 currency=self.currency,
72 raise TypeError('Incompatible currencies in add expression')
75 raise TypeError('In strict_mode only two moneys can be added')
77 return self.__sub__(CentCount(other, self.currency))
79 def __mul__(self, other):
80 if isinstance(other, CentCount):
81 raise TypeError('can not multiply monetary quantities')
84 centcount=int(self.centcount * float(other)),
85 currency=self.currency,
88 def __truediv__(self, other):
89 if isinstance(other, CentCount):
90 raise TypeError('can not divide monetary quantities')
93 centcount=int(float(self.centcount) / float(other)),
94 currency=self.currency,
98 return self.centcount.__int__()
101 return self.centcount.__float__() / 100.0
103 def truncate_fractional_cents(self):
105 self.centcount = int(math_utils.truncate_float(x))
106 return self.centcount
108 def round_fractional_cents(self):
110 self.centcount = int(round(x, 2))
111 return self.centcount
115 def __rsub__(self, other):
116 if isinstance(other, CentCount):
117 if self.currency == other.currency:
119 centcount=other.centcount - self.centcount,
120 currency=self.currency,
123 raise TypeError('Incompatible currencies in sub expression')
126 raise TypeError('In strict_mode only two moneys can be added')
129 centcount=int(other) - self.centcount,
130 currency=self.currency,
136 # Override comparison operators to also compare currency.
138 def __eq__(self, other):
141 if isinstance(other, CentCount):
142 return self.centcount == other.centcount and self.currency == other.currency
144 raise TypeError("In strict mode only two CentCounts can be compared")
146 return self.centcount == int(other)
148 def __ne__(self, other):
149 result = self.__eq__(other)
150 if result is NotImplemented:
154 def __lt__(self, other):
155 if isinstance(other, CentCount):
156 if self.currency == other.currency:
157 return self.centcount < other.centcount
159 raise TypeError('can not directly compare different currencies')
162 raise TypeError('In strict mode, only two CentCounts can be compated')
164 return self.centcount < int(other)
166 def __gt__(self, other):
167 if isinstance(other, CentCount):
168 if self.currency == other.currency:
169 return self.centcount > other.centcount
171 raise TypeError('can not directly compare different currencies')
174 raise TypeError('In strict mode, only two CentCounts can be compated')
176 return self.centcount > int(other)
178 def __le__(self, other):
179 return self < other or self == other
181 def __ge__(self, other):
182 return self > other or self == other
187 CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
188 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
191 def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
195 chunks = s.split(' ')
198 if CentCount.CENTCOUNT_RE.match(chunk) is not None:
199 centcount = int(float(chunk) * 100.0)
200 elif CentCount.CURRENCY_RE.match(chunk) is not None:
204 if centcount is not None and currency is not None:
205 return (centcount, currency)
206 elif centcount is not None:
207 return (centcount, 'USD')
211 def parse(cls, s: str) -> 'CentCount':
212 chunks = CentCount._parse(s)
213 if chunks is not None:
214 return CentCount(chunks[0], chunks[1])
215 raise Exception(f'Unable to parse money string "{s}"')