3 """A class to represent money. See also centcount.py"""
6 from decimal import Decimal
7 from typing import Optional, Tuple
13 """A class for representing monetary amounts potentially with
19 amount: Decimal = Decimal("0"),
20 currency: str = 'USD',
24 self.strict_mode = strict_mode
25 if isinstance(amount, str):
26 ret = Money._parse(amount)
28 raise Exception(f'Unable to parse money string "{amount}"')
31 if not isinstance(amount, Decimal):
32 amount = Decimal(float(amount))
35 self.currency: Optional[str] = None
37 self.currency = currency
40 a = float(self.amount)
43 if self.currency is not None:
44 return f'{s} {self.currency}'
49 return Money(amount=self.amount, currency=self.currency)
52 return Money(amount=-self.amount, currency=self.currency)
54 def __add__(self, other):
55 if isinstance(other, Money):
56 if self.currency == other.currency:
57 return Money(amount=self.amount + other.amount, currency=self.currency)
59 raise TypeError('Incompatible currencies in add expression')
62 raise TypeError('In strict_mode only two moneys can be added')
65 amount=self.amount + Decimal(float(other)),
66 currency=self.currency,
69 def __sub__(self, other):
70 if isinstance(other, Money):
71 if self.currency == other.currency:
72 return Money(amount=self.amount - other.amount, currency=self.currency)
74 raise TypeError('Incompatible currencies in add expression')
77 raise TypeError('In strict_mode only two moneys can be added')
80 amount=self.amount - Decimal(float(other)),
81 currency=self.currency,
84 def __mul__(self, other):
85 if isinstance(other, Money):
86 raise TypeError('can not multiply monetary quantities')
89 amount=self.amount * Decimal(float(other)),
90 currency=self.currency,
93 def __truediv__(self, other):
94 if isinstance(other, Money):
95 raise TypeError('can not divide monetary quantities')
98 amount=self.amount / Decimal(float(other)),
99 currency=self.currency,
103 return self.amount.__float__()
105 def truncate_fractional_cents(self):
107 self.amount = Decimal(math_utils.truncate_float(x))
110 def round_fractional_cents(self):
112 self.amount = Decimal(round(x, 2))
117 def __rsub__(self, other):
118 if isinstance(other, Money):
119 if self.currency == other.currency:
120 return Money(amount=other.amount - self.amount, currency=self.currency)
122 raise TypeError('Incompatible currencies in sub expression')
125 raise TypeError('In strict_mode only two moneys can be added')
128 amount=Decimal(float(other)) - self.amount,
129 currency=self.currency,
135 # Override comparison operators to also compare currency.
137 def __eq__(self, other):
140 if isinstance(other, Money):
141 return self.amount == other.amount and self.currency == other.currency
143 raise TypeError("In strict mode only two Moneys can be compared")
145 return self.amount == Decimal(float(other))
147 def __ne__(self, other):
148 result = self.__eq__(other)
149 if result is NotImplemented:
153 def __lt__(self, other):
154 if isinstance(other, Money):
155 if self.currency == other.currency:
156 return self.amount < other.amount
158 raise TypeError('can not directly compare different currencies')
161 raise TypeError('In strict mode, only two Moneys can be compated')
163 return self.amount < Decimal(float(other))
165 def __gt__(self, other):
166 if isinstance(other, Money):
167 if self.currency == other.currency:
168 return self.amount > other.amount
170 raise TypeError('can not directly compare different currencies')
173 raise TypeError('In strict mode, only two Moneys can be compated')
175 return self.amount > Decimal(float(other))
177 def __le__(self, other):
178 return self < other or self == other
180 def __ge__(self, other):
181 return self > other or self == other
183 def __hash__(self) -> int:
184 return hash(self.__repr__)
186 AMOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
187 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
190 def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
194 chunks = s.split(' ')
197 if Money.AMOUNT_RE.match(chunk) is not None:
198 amount = Decimal(chunk)
199 elif Money.CURRENCY_RE.match(chunk) is not None:
203 if amount is not None and currency is not None:
204 return (amount, currency)
205 elif amount is not None:
206 return (amount, 'USD')
210 def parse(cls, s: str) -> 'Money':
211 chunks = Money._parse(s)
212 if chunks is not None:
213 return Money(chunks[0], chunks[1])
214 raise Exception(f'Unable to parse money string "{s}"')