3 # © Copyright 2021-2022, Scott Gasch
5 """A class to represent money. See also centcount.py"""
8 from decimal import Decimal
9 from typing import Optional, Tuple
15 """A class for representing monetary amounts potentially with
21 amount: Decimal = Decimal("0"),
22 currency: str = 'USD',
26 self.strict_mode = strict_mode
27 if isinstance(amount, str):
28 ret = Money._parse(amount)
30 raise Exception(f'Unable to parse money string "{amount}"')
33 if not isinstance(amount, Decimal):
34 amount = Decimal(float(amount))
37 self.currency: Optional[str] = None
39 self.currency = currency
42 a = float(self.amount)
45 if self.currency is not None:
46 return f'{s} {self.currency}'
51 return Money(amount=self.amount, currency=self.currency)
54 return Money(amount=-self.amount, currency=self.currency)
56 def __add__(self, other):
57 if isinstance(other, Money):
58 if self.currency == other.currency:
59 return Money(amount=self.amount + other.amount, currency=self.currency)
61 raise TypeError('Incompatible currencies in add expression')
64 raise TypeError('In strict_mode only two moneys can be added')
67 amount=self.amount + Decimal(float(other)),
68 currency=self.currency,
71 def __sub__(self, other):
72 if isinstance(other, Money):
73 if self.currency == other.currency:
74 return Money(amount=self.amount - other.amount, currency=self.currency)
76 raise TypeError('Incompatible currencies in add expression')
79 raise TypeError('In strict_mode only two moneys can be added')
82 amount=self.amount - Decimal(float(other)),
83 currency=self.currency,
86 def __mul__(self, other):
87 if isinstance(other, Money):
88 raise TypeError('can not multiply monetary quantities')
91 amount=self.amount * Decimal(float(other)),
92 currency=self.currency,
95 def __truediv__(self, other):
96 if isinstance(other, Money):
97 raise TypeError('can not divide monetary quantities')
100 amount=self.amount / Decimal(float(other)),
101 currency=self.currency,
105 return self.amount.__float__()
107 def truncate_fractional_cents(self):
109 self.amount = Decimal(math_utils.truncate_float(x))
112 def round_fractional_cents(self):
114 self.amount = Decimal(round(x, 2))
119 def __rsub__(self, other):
120 if isinstance(other, Money):
121 if self.currency == other.currency:
122 return Money(amount=other.amount - self.amount, currency=self.currency)
124 raise TypeError('Incompatible currencies in sub expression')
127 raise TypeError('In strict_mode only two moneys can be added')
130 amount=Decimal(float(other)) - self.amount,
131 currency=self.currency,
137 # Override comparison operators to also compare currency.
139 def __eq__(self, other):
142 if isinstance(other, Money):
143 return self.amount == other.amount and self.currency == other.currency
145 raise TypeError("In strict mode only two Moneys can be compared")
147 return self.amount == Decimal(float(other))
149 def __ne__(self, other):
150 result = self.__eq__(other)
151 if result is NotImplemented:
155 def __lt__(self, other):
156 if isinstance(other, Money):
157 if self.currency == other.currency:
158 return self.amount < other.amount
160 raise TypeError('can not directly compare different currencies')
163 raise TypeError('In strict mode, only two Moneys can be compated')
165 return self.amount < Decimal(float(other))
167 def __gt__(self, other):
168 if isinstance(other, Money):
169 if self.currency == other.currency:
170 return self.amount > other.amount
172 raise TypeError('can not directly compare different currencies')
175 raise TypeError('In strict mode, only two Moneys can be compated')
177 return self.amount > Decimal(float(other))
179 def __le__(self, other):
180 return self < other or self == other
182 def __ge__(self, other):
183 return self > other or self == other
185 def __hash__(self) -> int:
186 return hash(self.__repr__)
188 AMOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
189 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
192 def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
196 chunks = s.split(' ')
199 if Money.AMOUNT_RE.match(chunk) is not None:
200 amount = Decimal(chunk)
201 elif Money.CURRENCY_RE.match(chunk) is not None:
205 if amount is not None and currency is not None:
206 return (amount, currency)
207 elif amount is not None:
208 return (amount, 'USD')
212 def parse(cls, s: str) -> 'Money':
213 chunks = Money._parse(s)
214 if chunks is not None:
215 return Money(chunks[0], chunks[1])
216 raise Exception(f'Unable to parse money string "{s}"')