diff --git a/piggybank/currency.py b/piggybank/currency.py new file mode 100644 index 0000000..f61e114 --- /dev/null +++ b/piggybank/currency.py @@ -0,0 +1,128 @@ +"""Currency class and a set of exceptions. + +Face values are stored as decimals*. + +* Decimal is used to avoid problems of rounding float numbers. So first two +digits to the right are used to store a fraction part. You can simply divide +this number by 100 to get a floating-point number. + +Currency string has following format: + ;;;{},{} + ; + +{} with currency symbol used as a placeholder for where to put digits. + +Example: + RUB;Russian ruble;8;{}коп.,{}₽;1,5,10,50,100,200,500,1000 + USD;US Dollar;6;{}¢,${};1,5,10,25,50,100 +""" + +from __future__ import annotations +from operator import mul +from typing import Dict, List, TypedDict + +__all__ = [ "Currency", "BaseCurrencyError", "CurrencyIsNotSupportedError", + "CurrenciesCoinCountMismatchError", "CurrencyMismatchError"] + + +class BaseCurrencyError(Exception): + """Base class for all currency exeptions.""" + def __init__(self, message=None, *args, **kwargs) -> None: + if message is None: + message = self.__doc__ + super().__init__(message, *args, **kwargs) + + +class CurrencyIsNotSupportedError(BaseCurrencyError): + """Currency is not supported.""" + + +class CurrencyMismatchError(BaseCurrencyError): + """Currencies doesn't match, but they must do so.""" + def __init__(self, extra=None): + if not extra is None: + super().__init__(f"{self.__doc__} {extra}") + else: + super().__init__(self.__doc__) + + +class CurrenciesCoinCountMismatchError(BaseCurrencyError): + """Length of list supplied doesn't corresponds coins count.""" + + +class Currency: + """Currency's representation and methods to work with it. + + Arguments: + iso_code -- ISO code of a currency; + name -- full name of a currency; + coins_count -- number of face values of coins; + fraction_symbol -- a symbol or word for a fraction of currency (e.g. c, cent); + currency_symbol -- a symbol or word for a currency (e.g. $); + face_values -- list of values for each face value in decimal + (e.g. $476.40 is 47640); + """ + def __init__(self, iso_code: str, name: str, coins_count: int, + fraction_symbol: str, currency_symbol: str, + face_values: List[int]): + self._iso = iso_code.upper() + self._name = name + self._coins_count = coins_count + self._fraction_symbol = fraction_symbol + self._currency_symbol = currency_symbol + self._face_values = face_values + + @property + def iso(self) -> str: + """ISO code of a currency.""" + return self._iso + + @property + def name(self) -> str: + """Full name of a currency.""" + return self._name + + @property + def count(self): + """Count of coins of a currency.""" + return self._coins_count + + @property + def fraction_symbol(self) -> str: + """Returns a symbol or word for a fraction of a currency.""" + return self._fraction_symbol + + @property + def currency_symbol(self) -> str: + """Returns a symbol or word for a currency.""" + return self._currency_symbol + + @property + def face_values(self) -> List[int]: + """Face values of coins. From least to most.""" + return self._face_values + + def multiply(self, lst: List[int]) -> List[float]: + """Multiplies a list by face values. Length of list must match + currency's coins count.""" + if len(lst) != len(self): + raise CurrenciesCoinCountMismatchError + return list(map(mul, lst, self.face_values)) + + @staticmethod + def from_string(currency: str) -> Currency: + """Creates a Currency object from its string representation.""" + iso, name, count, frac_sym, cur_sym, fv = currency.split(";") + return Currency(iso, name, int(count), frac_sym, cur_sym, + list(map(int, fv.split(",")))) + + def __mul__(self, lst: List[int]) -> List[float]: + return self.multiply(lst) + + def __len__(self) -> int: + return self.count + + def __str__(self): + return f"{self.iso};{self.name};{self.count};" \ + f"{self.fraction_symbol},{self.currency_symbol};" \ + f"{','.join(list(map(str, self.face_values)))}"