3 from decimal import Decimal
5 from typing import Optional, TypeVar, Tuple
10 T = TypeVar('T', bound='Money')
14 """A class for representing monetary amounts potentially with
20 amount: Decimal = Decimal("0"),
21 currency: str = 'USD',
25 self.strict_mode = strict_mode
26 if isinstance(amount, str):
27 ret = Money._parse(amount)
29 raise Exception(f'Unable to parse money string "{amount}"')
32 if not isinstance(amount, Decimal):
33 amount = Decimal(float(amount))
36 self.currency: Optional[str] = None
38 self.currency: Optional[str] = currency
41 a = float(self.amount)
44 if self.currency is not None:
45 return '%s %s' % (s, self.currency)
50 return Money(amount=self.amount, currency=self.currency)
53 return Money(amount=-self.amount, currency=self.currency)
55 def __add__(self, other):
56 if isinstance(other, Money):
57 if self.currency == other.currency:
59 amount = self.amount + other.amount,
60 currency = self.currency
63 raise TypeError('Incompatible currencies in add expression')
66 raise TypeError('In strict_mode only two moneys can be added')
69 amount = self.amount + Decimal(float(other)),
70 currency = self.currency
73 def __sub__(self, other):
74 if isinstance(other, Money):
75 if self.currency == other.currency:
77 amount = self.amount - other.amount,
78 currency = self.currency
81 raise TypeError('Incompatible currencies in add expression')
84 raise TypeError('In strict_mode only two moneys can be added')
87 amount = self.amount - Decimal(float(other)),
88 currency = self.currency
91 def __mul__(self, other):
92 if isinstance(other, Money):
93 raise TypeError('can not multiply monetary quantities')
96 amount = self.amount * Decimal(float(other)),
97 currency = self.currency
100 def __truediv__(self, other):
101 if isinstance(other, Money):
102 raise TypeError('can not divide monetary quantities')
105 amount = self.amount / Decimal(float(other)),
106 currency = self.currency
110 return self.amount.__float__()
112 def truncate_fractional_cents(self):
114 self.amount = Decimal(math_utils.truncate_float(x))
117 def round_fractional_cents(self):
119 self.amount = Decimal(round(x, 2))
124 def __rsub__(self, other):
125 if isinstance(other, Money):
126 if self.currency == other.currency:
128 amount = other.amount - self.amount,
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 amount = Decimal(float(other)) - self.amount,
139 currency = self.currency
145 # Override comparison operators to also compare currency.
147 def __eq__(self, other):
150 if isinstance(other, Money):
152 self.amount == other.amount and
153 self.currency == other.currency
156 raise TypeError("In strict mode only two Moneys can be compared")
158 return self.amount == Decimal(float(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, Money):
168 if self.currency == other.currency:
169 return self.amount < other.amount
171 raise TypeError('can not directly compare different currencies')
174 raise TypeError('In strict mode, only two Moneys can be compated')
176 return self.amount < Decimal(float(other))
178 def __gt__(self, other):
179 if isinstance(other, Money):
180 if self.currency == other.currency:
181 return self.amount > other.amount
183 raise TypeError('can not directly compare different currencies')
186 raise TypeError('In strict mode, only two Moneys can be compated')
188 return self.amount > Decimal(float(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 AMOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$")
200 CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$")
203 def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
207 chunks = s.split(' ')
210 if Money.AMOUNT_RE.match(chunk) is not None:
211 amount = Decimal(chunk)
212 elif Money.CURRENCY_RE.match(chunk) is not None:
216 if amount is not None and currency is not None:
217 return (amount, currency)
218 elif amount is not None:
219 return (amount, 'USD')
223 def parse(cls, s: str) -> T:
224 chunks = Money._parse(s)
225 if chunks is not None:
226 return Money(chunks[0], chunks[1])
227 raise Exception(f'Unable to parse money string "{s}"')