4 from typing import Optional, TypeVar, Tuple
9 T = TypeVar('T', bound='CentCount')
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.
21 currency: str = 'USD',
25 self.strict_mode = strict_mode
26 if isinstance(centcount, str):
27 ret = CentCount._parse(centcount)
29 raise Exception(f'Unable to parse money string "{centcount}"')
32 if isinstance(centcount, float):
33 centcount = int(centcount * 100.0)
34 if not isinstance(centcount, int):
35 centcount = int(centcount)
36 self.centcount = centcount
38 self.currency: Optional[str] = None
40 self.currency: Optional[str] = currency
43 a = float(self.centcount)
47 if self.currency is not None:
48 return '%s %s' % (s, self.currency)
53 return CentCount(centcount=self.centcount, currency=self.currency)
56 return CentCount(centcount=-self.centcount, currency=self.currency)
58 def __add__(self, other):
59 if isinstance(other, CentCount):
60 if self.currency == other.currency:
62 centcount = self.centcount + other.centcount,
63 currency = self.currency
66 raise TypeError('Incompatible currencies in add expression')
69 raise TypeError('In strict_mode only two moneys can be added')
71 return self.__add__(CentCount(other, self.currency))
73 def __sub__(self, other):
74 if isinstance(other, CentCount):
75 if self.currency == other.currency:
77 centcount = self.centcount - other.centcount,
78 currency = self.currency
81 raise TypeError('Incompatible currencies in add expression')
84 raise TypeError('In strict_mode only two moneys can be added')
86 return self.__sub__(CentCount(other, self.currency))
88 def __mul__(self, other):
89 if isinstance(other, CentCount):
90 raise TypeError('can not multiply monetary quantities')
93 centcount = int(self.centcount * float(other)),
94 currency = self.currency
97 def __truediv__(self, other):
98 if isinstance(other, CentCount):
99 raise TypeError('can not divide monetary quantities')
102 centcount = int(float(self.centcount) / float(other)),
103 currency = self.currency
107 return self.centcount.__int__()
110 return self.centcount.__float__() / 100.0
112 def truncate_fractional_cents(self):
114 self.centcount = int(math_utils.truncate_float(x))
115 return self.centcount
117 def round_fractional_cents(self):
119 self.centcount = int(round(x, 2))
120 return self.centcount
124 def __rsub__(self, other):
125 if isinstance(other, CentCount):
126 if self.currency == other.currency:
128 centcount = other.centcount - self.centcount,
129 currency = self.currency
132 raise TypeError('Incompatible currencies in sub expression')
135 raise TypeError('In strict_mode only two moneys can be added')
138 centcount = int(other) - self.centcount,
139 currency = self.currency
145 # Override comparison operators to also compare currency.
147 def __eq__(self, other):
150 if isinstance(other, CentCount):
152 self.centcount == other.centcount and
153 self.currency == other.currency
156 raise TypeError("In strict mode only two CentCounts can be compared")
158 return self.centcount == int(other)
160 def __ne__(self, other):
161 result = self.__eq__(other)
162 if result is NotImplemented:
166 def __lt__(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 __gt__(self, other):
179 if isinstance(other, CentCount):
180 if self.currency == other.currency:
181 return self.centcount > other.centcount
183 raise TypeError('can not directly compare different currencies')
186 raise TypeError('In strict mode, only two CentCounts can be compated')
188 return self.centcount > int(other)
190 def __le__(self, other):
191 return self < other or self == other
193 def __ge__(self, other):
194 return self > other or self == other
199 CENTCOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
200 CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
203 def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
207 chunks = s.split(' ')
210 if CentCount.CENTCOUNT_RE.match(chunk) is not None:
211 centcount = int(float(chunk) * 100.0)
212 elif CentCount.CURRENCY_RE.match(chunk) is not None:
216 if centcount is not None and currency is not None:
217 return (centcount, currency)
218 elif centcount is not None:
219 return (centcount, 'USD')
223 def parse(cls, s: str) -> T:
224 chunks = CentCount._parse(s)
225 if chunks is not None:
226 return CentCount(chunks[0], chunks[1])
227 raise Exception(f'Unable to parse money string "{s}"')