X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=type%2Fmoney.py;fp=type%2Fmoney.py;h=c77a9389391201c47528bcc1f87bbf7e79134e02;hb=4faa994d32223c8d560d9dad0ca90a3f7eb10d6a;hp=0000000000000000000000000000000000000000;hpb=c79ecbf708a63a54a9c3e8d189b65d4794930082;p=python_utils.git diff --git a/type/money.py b/type/money.py new file mode 100644 index 0000000..c77a938 --- /dev/null +++ b/type/money.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 + +from decimal import Decimal +import re +from typing import Optional, TypeVar, Tuple + +import math_utils + + +T = TypeVar('T', bound='Money') + + +class Money(object): + """A class for representing monetary amounts potentially with + different currencies. + """ + + def __init__ ( + self, + amount: Decimal = Decimal("0.0"), + currency: str = 'USD', + *, + strict_mode = False + ): + self.strict_mode = strict_mode + if isinstance(amount, str): + ret = Money._parse(amount) + if ret is None: + raise Exception(f'Unable to parse money string "{amount}"') + amount = ret[0] + currency = ret[1] + if not isinstance(amount, Decimal): + amount = Decimal(float(amount)) + self.amount = amount + if not currency: + self.currency: Optional[str] = None + else: + self.currency: Optional[str] = currency + + def __repr__(self): + a = float(self.amount) + a = round(a, 2) + s = f'{a:,.2f}' + if self.currency is not None: + return '%s %s' % (s, self.currency) + else: + return '$%s' % s + + def __pos__(self): + return Money(amount=self.amount, currency=self.currency) + + def __neg__(self): + return Money(amount=-self.amount, currency=self.currency) + + def __add__(self, other): + if isinstance(other, Money): + if self.currency == other.currency: + return Money( + amount = self.amount + other.amount, + currency = self.currency + ) + else: + raise TypeError('Incompatible currencies in add expression') + else: + if self.strict_mode: + raise TypeError('In strict_mode only two moneys can be added') + else: + return Money( + amount = self.amount + Decimal(float(other)), + currency = self.currency + ) + + def __sub__(self, other): + if isinstance(other, Money): + if self.currency == other.currency: + return Money( + amount = self.amount - other.amount, + currency = self.currency + ) + else: + raise TypeError('Incompatible currencies in add expression') + else: + if self.strict_mode: + raise TypeError('In strict_mode only two moneys can be added') + else: + return Money( + amount = self.amount - Decimal(float(other)), + currency = self.currency + ) + + def __mul__(self, other): + if isinstance(other, Money): + raise TypeError('can not multiply monetary quantities') + else: + return Money( + amount = self.amount * Decimal(float(other)), + currency = self.currency + ) + + def __truediv__(self, other): + if isinstance(other, Money): + raise TypeError('can not divide monetary quantities') + else: + return Money( + amount = self.amount / Decimal(float(other)), + currency = self.currency + ) + + def __float__(self): + return self.amount.__float__() + + def truncate_fractional_cents(self): + x = float(self) + self.amount = Decimal(math_utils.truncate_float(x)) + return self.amount + + def round_fractional_cents(self): + x = float(self) + self.amount = Decimal(round(x, 2)) + return self.amount + + __radd__ = __add__ + + def __rsub__(self, other): + if isinstance(other, Money): + if self.currency == other.currency: + return Money( + amount = other.amount - self.amount, + currency = self.currency + ) + else: + raise TypeError('Incompatible currencies in sub expression') + else: + if self.strict_mode: + raise TypeError('In strict_mode only two moneys can be added') + else: + return Money( + amount = Decimal(float(other)) - self.amount, + currency = self.currency + ) + + __rmul__ = __mul__ + + # + # Override comparison operators to also compare currency. + # + def __eq__(self, other): + if other is None: + return False + if isinstance(other, Money): + return ( + self.amount == other.amount and + self.currency == other.currency + ) + if self.strict_mode: + raise TypeError("In strict mode only two Moneys can be compared") + else: + return self.amount == Decimal(float(other)) + + def __ne__(self, other): + result = self.__eq__(other) + if result is NotImplemented: + return result + return not result + + def __lt__(self, other): + if isinstance(other, Money): + if self.currency == other.currency: + return self.amount < other.amount + else: + raise TypeError('can not directly compare different currencies') + else: + if self.strict_mode: + raise TypeError('In strict mode, only two Moneys can be compated') + else: + return self.amount < Decimal(float(other)) + + def __gt__(self, other): + if isinstance(other, Money): + if self.currency == other.currency: + return self.amount > other.amount + else: + raise TypeError('can not directly compare different currencies') + else: + if self.strict_mode: + raise TypeError('In strict mode, only two Moneys can be compated') + else: + return self.amount > Decimal(float(other)) + + def __le__(self, other): + return self < other or self == other + + def __ge__(self, other): + return self > other or self == other + + def __hash__(self): + return self.__repr__ + + AMOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$") + CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$") + + @classmethod + def _parse(cls, s: str) -> Optional[Tuple[Decimal, str]]: + amount = None + currency = None + s = s.strip() + chunks = s.split(' ') + try: + for chunk in chunks: + if Money.AMOUNT_RE.match(chunk) is not None: + amount = Decimal(chunk) + elif Money.CURRENCY_RE.match(chunk) is not None: + currency = chunk + except: + pass + if amount is not None and currency is not None: + return (amount, currency) + elif amount is not None: + return (amount, 'USD') + return None + + @classmethod + def parse(cls, s: str) -> T: + chunks = Money._parse(s) + if chunks is not None: + return Money(chunks[0], chunks[1]) + raise Exception(f'Unable to parse money string "{s}"')