X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=type%2Fcentcount.py;fp=type%2Fcentcount.py;h=4181721bbc0d4ab7efe38608b821faa26ecf5bae;hb=4faa994d32223c8d560d9dad0ca90a3f7eb10d6a;hp=0000000000000000000000000000000000000000;hpb=c79ecbf708a63a54a9c3e8d189b65d4794930082;p=python_utils.git diff --git a/type/centcount.py b/type/centcount.py new file mode 100644 index 0000000..4181721 --- /dev/null +++ b/type/centcount.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 + +import re +from typing import Optional, TypeVar, Tuple + +import math_utils + + +T = TypeVar('T', bound='CentCount') + + +class CentCount(object): + """A class for representing monetary amounts potentially with + different currencies. + """ + + def __init__ ( + self, + centcount, + currency: str = 'USD', + *, + strict_mode = False + ): + self.strict_mode = strict_mode + if isinstance(centcount, str): + ret = CentCount._parse(centcount) + if ret is None: + raise Exception(f'Unable to parse money string "{centcount}"') + centcount = ret[0] + currency = ret[1] + if isinstance(centcount, float): + centcount = int(centcount * 100.0) + if not isinstance(centcount, int): + centcount = int(centcount) + self.centcount = centcount + if not currency: + self.currency: Optional[str] = None + else: + self.currency: Optional[str] = currency + + def __repr__(self): + a = float(self.centcount) + a /= 100 + 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 CentCount(centcount=self.centcount, currency=self.currency) + + def __neg__(self): + return CentCount(centcount=-self.centcount, currency=self.currency) + + def __add__(self, other): + if isinstance(other, CentCount): + if self.currency == other.currency: + return CentCount( + centcount = self.centcount + other.centcount, + 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 self.__add__(CentCount(other, self.currency)) + + def __sub__(self, other): + if isinstance(other, CentCount): + if self.currency == other.currency: + return CentCount( + centcount = self.centcount - other.centcount, + 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 self.__sub__(CentCount(other, self.currency)) + + def __mul__(self, other): + if isinstance(other, CentCount): + raise TypeError('can not multiply monetary quantities') + else: + return CentCount( + centcount = int(self.centcount * float(other)), + currency = self.currency + ) + + def __truediv__(self, other): + if isinstance(other, CentCount): + raise TypeError('can not divide monetary quantities') + else: + return CentCount( + centcount = int(float(self.centcount) / float(other)), + currency = self.currency + ) + + def __int__(self): + return self.centcount.__int__() + + def __float__(self): + return self.centcount.__float__() / 100.0 + + def truncate_fractional_cents(self): + x = int(self) + self.centcount = int(math_utils.truncate_float(x)) + return self.centcount + + def round_fractional_cents(self): + x = int(self) + self.centcount = int(round(x, 2)) + return self.centcount + + __radd__ = __add__ + + def __rsub__(self, other): + if isinstance(other, CentCount): + if self.currency == other.currency: + return CentCount( + centcount = other.centcount - self.centcount, + 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 CentCount( + centcount = int(other) - self.centcount, + 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, CentCount): + return ( + self.centcount == other.centcount and + self.currency == other.currency + ) + if self.strict_mode: + raise TypeError("In strict mode only two CentCounts can be compared") + else: + return self.centcount == int(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, CentCount): + if self.currency == other.currency: + return self.centcount < other.centcount + else: + raise TypeError('can not directly compare different currencies') + else: + if self.strict_mode: + raise TypeError('In strict mode, only two CentCounts can be compated') + else: + return self.centcount < int(other) + + def __gt__(self, other): + if isinstance(other, CentCount): + if self.currency == other.currency: + return self.centcount > other.centcount + else: + raise TypeError('can not directly compare different currencies') + else: + if self.strict_mode: + raise TypeError('In strict mode, only two CentCounts can be compated') + else: + return self.centcount > int(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__ + + CENTCOUNT_RE = re.compile("^([+|-]?)(\d+)(\.\d+)$") + CURRENCY_RE = re.compile("^[A-Z][A-Z][A-Z]$") + + @classmethod + def _parse(cls, s: str) -> Optional[Tuple[int, str]]: + centcount = None + currency = None + s = s.strip() + chunks = s.split(' ') + try: + for chunk in chunks: + if CentCount.CENTCOUNT_RE.match(chunk) is not None: + centcount = int(float(chunk) * 100.0) + elif CentCount.CURRENCY_RE.match(chunk) is not None: + currency = chunk + except: + pass + if centcount is not None and currency is not None: + return (centcount, currency) + elif centcount is not None: + return (centcount, 'USD') + return None + + @classmethod + def parse(cls, s: str) -> T: + chunks = CentCount._parse(s) + if chunks is not None: + return CentCount(chunks[0], chunks[1]) + raise Exception(f'Unable to parse money string "{s}"')