From a8ec679349c3eb9c33a9d33e247fd86cb8e53f81 Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Thu, 14 Mar 2019 12:23:57 +0100 Subject: o Adding PriceBreak. Parsing price breaks from DK. o Adding Money type with parsing. --- src/ee/money.py | 297 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/ee/money.py (limited to 'src/ee/money.py') diff --git a/src/ee/money.py b/src/ee/money.py new file mode 100644 index 0000000..139d0d7 --- /dev/null +++ b/src/ee/money.py @@ -0,0 +1,297 @@ +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 -- cgit v1.2.3