from decimal import Decimal from typing import Union, Optional, Mapping, Set from ee import EeException __all__ = [ "Money", "MoneyContext", "get_default_context", "set_default_context", ] def _parse(amount: str) -> (str, str): def read_space(): nonlocal idx while idx < end: if amount[idx] != " ": break idx += 1 def read_num(): nonlocal idx tmp = "" while idx < end: c_ = amount[idx] if not c_.isnumeric(): break tmp += c_ idx += 1 return tmp def read_word(): nonlocal idx tmp = "" while idx < end: c_ = amount[idx] if c_.isnumeric() or c_ in (" ", ",", "."): break tmp += c_ idx += 1 return tmp amount = amount.strip() idx = 0 end = len(amount) if end == 0: return None, None # extract leading currency prefix = read_word() read_space() # If the number starts with '.' or ',' we assume it is a whole separator # if idx < end: # c = amount[idx] # if c == "." or c == ",": # s1 = c # idx += 1 num = read_num() # if len(num) > 3: # return None, None # First and second separators s1 = s2 = None s0s = [] # stuff before first separator s1s = [] s2s = [] if num: s0s.append(num) while idx < end: c = amount[idx] is_separator = c in (",", ".") if not is_separator and c == " ": next_c = amount[idx + 1] if idx + 1 < end else None if next_c is None or not next_c.isnumeric(): idx += 1 break is_separator = True if not is_separator: return None, None if not s1: s1 = c elif s1 and c == s1: pass elif s1 and c != s1 and not s2: s2 = c else: return None, None idx += 1 num = read_num() if not num: if idx == end: break return None, None if s2: s2s.append(num) else: s1s.append(num) del num if s1 == ' ': s1 = s2 s2 = None s0s.extend(s1s) s1s = s2s s2s = [] if not s1 and not s2: wholes = "".join(s0s) parts = "0" elif s1 and not s2: wholes = "".join(s0s) parts = "".join(s1s) elif s1 and s2: wholes = "".join(s0s) + "".join(s1s) parts = "".join(s2s) else: return None, None read_space() postfix = read_word() # Check that there is no junk left if amount[idx:].strip(): return None, None # wholes = wholes if wholes else "0" # parts = parts if parts else "0" currency = None if len(prefix): currency = prefix if len(postfix): if currency: return None, None currency = postfix while parts.endswith("0"): parts = parts[:-1] return wholes + "." + parts, currency class Money(object): def __init__(self, amount: Decimal, currency: Optional[str] = None): self.amount: Decimal = amount self.currency = currency def assert_same_currency(self, other): if isinstance(other, int): return if self.currency is None and other.currency is None: return if self.currency == other.currency: return raise EeException("Can't relate to instances of Money with different currencies.") def __truediv__(self, other: Union["Money", int]): self.assert_same_currency(other) if isinstance(other, int): return Money(amount=self.amount.__truediv__(other), currency=self.currency) return Money(amount=self.amount.__truediv__(other), currency=self.currency) def __mul__(self, other: Union["Money", int]): self.assert_same_currency(other) if isinstance(other, int): return Money(amount=self.amount.__mul__(other), currency=self.currency) return Money(amount=self.amount.__mul__(other), currency=self.currency) def _amount_str(self): amount = str(self.amount) (sign, _int, _exp) = self.amount.as_tuple() if _exp >= 0: return amount while amount.endswith("0"): amount = amount[0:-1] if amount.endswith("."): amount = amount[0:-1] return amount def __repr__(self): return self._amount_str() + ((" " + self.currency) if self.currency else "") def __str__(self): amount = self._amount_str() if self.currency is None: return amount first, last = (self.currency, amount) if self.currency in _prefix_currencies else (amount, self.currency) return first + " " + last def __eq__(self, other: "Money"): if not isinstance(other, Money): return False return self.amount == other.amount and self.currency == other.currency class MoneyContext(object): def __init__(self, symbols_to_currency: Mapping[str, str], prefix_currencies: Set[str]): self.symbols_to_currency = symbols_to_currency self.prefix_currencies = prefix_currencies @staticmethod def parse(amount: Union[str, int, float, Decimal], currency: Optional[str] = None) -> Money: m = MoneyContext.try_parse(amount, currency) if m is None: raise EeException("Could not parse money: '{}'".format(amount)) return m @staticmethod def try_parse(amount: Union[str, int, float, Decimal], currency: Optional[str] = None) -> Optional[Money]: if isinstance(amount, str): (a, c) = _parse(amount) if a is None: return None amount = Decimal(a) if c is not None: if currency is not None and currency != c: return None currency = c elif isinstance(amount, int) or isinstance(amount, float): amount = Decimal(amount) elif isinstance(amount, Decimal): pass else: raise EeException("Unsupported value type: {}".format(type(amount))) if currency is not None: currency = _symbols_to_currency.get(currency, currency) return Money(amount, currency) _symbols_to_currency = { "$": "USD" } _prefix_currencies = { "USD", } _default_context = None def get_default_context(): global _default_context if _default_context is None: _default_context = MoneyContext(_symbols_to_currency, _prefix_currencies) return _default_context def set_default_context(ctx: MoneyContext): global _default_context _default_context = ctx