4 from decimal import Decimal
5 from typing import Optional, Tuple, TypeVar
11 """A class for representing monetary amounts potentially with
17 amount: Decimal = Decimal("0"),
18 currency: str = 'USD',
22 self.strict_mode = strict_mode
23 if isinstance(amount, str):
24 ret = Money._parse(amount)
26 raise Exception(f'Unable to parse money string "{amount}"')
29 if not isinstance(amount, Decimal):
30 amount = Decimal(float(amount))
33 self.currency: Optional[str] = None
35 self.currency = currency
38 a = float(self.amount)
41 if self.currency is not None:
42 return '%s %s' % (s, self.currency)
47 return Money(amount=self.amount, currency=self.currency)
50 return Money(amount=-self.amount, currency=self.currency)
52 def __add__(self, other):
53 if isinstance(other, Money):
54 if self.currency == other.currency:
55 return Money(amount=self.amount + other.amount, currency=self.currency)
57 raise TypeError('Incompatible currencies in add expression')
60 raise TypeError('In strict_mode only two moneys can be added')
63 amount=self.amount + Decimal(float(other)),
64 currency=self.currency,
67 def __sub__(self, other):
68 if isinstance(other, Money):
69 if self.currency == other.currency:
70 return Money(amount=self.amount - other.amount, currency=self.currency)
72 raise TypeError('Incompatible currencies in add expression')
75 raise TypeError('In strict_mode only two moneys can be added')
78 amount=self.amount - Decimal(float(other)),
79 currency=self.currency,
82 def __mul__(self, other):
83 if isinstance(other, Money):
84 raise TypeError('can not multiply monetary quantities')
87 amount=self.amount * Decimal(float(other)),
88 currency=self.currency,
91 def __truediv__(self, other):
92 if isinstance(other, Money):
93 raise TypeError('can not divide monetary quantities')
96 amount=self.amount / Decimal(float(other)),
97 currency=self.currency,
101 return self.amount.__float__()
103 def truncate_fractional_cents(self):
105 self.amount = Decimal(math_utils.truncate_float(x))
108 def round_fractional_cents(self):
110 self.amount = Decimal(round(x, 2))
115 def __rsub__(self, other):
116 if isinstance(other, Money):
117 if self.currency == other.currency:
118 return Money(amount=other.amount - self.amount, currency=self.currency)
120 raise TypeError('Incompatible currencies in sub expression')
123 raise TypeError('In strict_mode only two moneys can be added')
126 amount=Decimal(float(other)) - self.amount,
127 currency=self.currency,
133 # Override comparison operators to also compare currency.
135 def __eq__(self, other):
138 if isinstance(other, Money):
139 return self.amount == other.amount and self.currency == other.currency
141 raise TypeError("In strict mode only two Moneys can be compared")
143 return self.amount == Decimal(float(other))
145 def __ne__(self, other):
146 result = self.__eq__(other)
147 if result is NotImplemented:
151 def __lt__(self, other):
152 if isinstance(other, Money):
153 if self.currency == other.currency:
154 return self.amount < other.amount
156 raise TypeError('can not directly compare different currencies')
159 raise TypeError('In strict mode, only two Moneys can be compated')
161 return self.amount < Decimal(float(other))
163 def __gt__(self, other):
164 if isinstance(other, Money):
165 if self.currency == other.currency:
166 return self.amount > other.amount
168 raise TypeError('can not directly compare different currencies')
171 raise TypeError('In strict mode, only two Moneys can be compated')
173 return self.amount > Decimal(float(other))
175 def __le__(self, other):
176 return self < other or self == other
178 def __ge__(self, other):
179 return self > other or self == other
184 AMOUNT_RE = re.compile(r"^([+|-]?)(\d+)(\.\d+)$")
185 CURRENCY_RE = re.compile(r"^[A-Z][A-Z][A-Z]$")
188 def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]:
192 chunks = s.split(' ')
195 if Money.AMOUNT_RE.match(chunk) is not None:
196 amount = Decimal(chunk)
197 elif Money.CURRENCY_RE.match(chunk) is not None:
201 if amount is not None and currency is not None:
202 return (amount, currency)
203 elif amount is not None:
204 return (amount, 'USD')
208 def parse(cls, s: str) -> 'Money':
209 chunks = Money._parse(s)
210 if chunks is not None:
211 return Money(chunks[0], chunks[1])
212 raise Exception(f'Unable to parse money string "{s}"')