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, currency=self.currency
56 raise TypeError('Incompatible currencies in add expression')
59 raise TypeError('In strict_mode only two moneys can be added')
61 return self.__add__(CentCount(other, self.currency))
63 def __sub__(self, other):
64 if isinstance(other, CentCount):
65 if self.currency == other.currency:
67 centcount=self.centcount - other.centcount, currency=self.currency
70 raise TypeError('Incompatible currencies in add expression')
73 raise TypeError('In strict_mode only two moneys can be added')
75 return self.__sub__(CentCount(other, self.currency))
77 def __mul__(self, other):
78 if isinstance(other, CentCount):
79 raise TypeError('can not multiply monetary quantities')
82 centcount=int(self.centcount * float(other)), currency=self.currency
85 def __truediv__(self, other):
86 if isinstance(other, CentCount):
87 raise TypeError('can not divide monetary quantities')
90 centcount=int(float(self.centcount) / float(other)),
91 currency=self.currency,
95 return self.centcount.__int__()
98 return self.centcount.__float__() / 100.0
100 def truncate_fractional_cents(self):
102 self.centcount = int(math_utils.truncate_float(x))
103 return self.centcount
105 def round_fractional_cents(self):
107 self.centcount = int(round(x, 2))
108 return self.centcount
112 def __rsub__(self, other):
113 if isinstance(other, CentCount):
114 if self.currency == other.currency:
116 centcount=other.centcount - self.centcount, currency=self.currency
119 raise TypeError('Incompatible currencies in sub expression')
122 raise TypeError('In strict_mode only two moneys can be added')
125 centcount=int(other) - self.centcount, currency=self.currency
131 # Override comparison operators to also compare currency.
133 def __eq__(self, other):
136 if isinstance(other, CentCount):
137 return self.centcount == other.centcount and self.currency == other.currency
139 raise TypeError("In strict mode only two CentCounts can be compared")
141 return self.centcount == int(other)
143 def __ne__(self, other):
144 result = self.__eq__(other)
145 if result is NotImplemented:
149 def __lt__(self, other):
150 if isinstance(other, CentCount):
151 if self.currency == other.currency:
152 return self.centcount < other.centcount
154 raise TypeError('can not directly compare different currencies')
157 raise TypeError('In strict mode, only two CentCounts can be compated')
159 return self.centcount < int(other)
161 def __gt__(self, other):
162 if isinstance(other, CentCount):
163 if self.currency == other.currency:
164 return self.centcount > other.centcount
166 raise TypeError('can not directly compare different currencies')
169 raise TypeError('In strict mode, only two CentCounts can be compated')
171 return self.centcount > int(other)
173 def __le__(self, other):
174 return self < other or self == other
176 def __ge__(self, other):
177 return self > other or self == other
182 CENTCOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
183 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
186 def _parse(cls, s: str) -> Optional[Tuple[int, str]]:
190 chunks = s.split(' ')
193 if CentCount.CENTCOUNT_RE.match(chunk) is not None:
194 centcount = int(float(chunk) * 100.0)
195 elif CentCount.CURRENCY_RE.match(chunk) is not None:
199 if centcount is not None and currency is not None:
200 return (centcount, currency)
201 elif centcount is not None:
202 return (centcount, 'USD')
206 def parse(cls, s: str) -> 'CentCount':
207 chunks = CentCount._parse(s)
208 if chunks is not None:
209 return CentCount(chunks[0], chunks[1])
210 raise Exception(f'Unable to parse money string "{s}"')