import datetime from decimal import Decimal from numbers import Number from typing import Iterable, Set, Union from money.currency import Currency from xexe.money_amount import DEFAULT_MONEY_PRECISION_POSITIONS, MoneyAmount class ExchangeRate: def __init__( self, from_currency: Currency, to_currency: Currency, rate: Union[MoneyAmount, Number, str], rate_date: datetime.date, ) -> None: self.from_currency = from_currency self.to_currency = to_currency if not isinstance(rate, MoneyAmount): rate = MoneyAmount(amount=rate, currency=to_currency) self.rate = rate self.rate_date = rate_date @property def descriptor(self) -> str: return ( str(self.from_currency.value) + str(self.to_currency.value) + str(self.rate_date.strftime("%Y-%m-%d")) ) @property def amount(self) -> Decimal: return self.rate.amount class ExchangeRates: def __init__(self, rates: Union[Iterable[ExchangeRate], None] = None): self._rate_index = {} if rates is not None: for rate in rates: if not isinstance(rate, ExchangeRate): raise TypeError("ExchangeRates can only hold Rates.") self._rate_index[rate.descriptor] = rate @property def present_currencies(self) -> Set[Currency]: present_currencies = set() for rate in self: present_currencies.add(rate.from_currency) present_currencies.add(rate.to_currency) return present_currencies @property def present_dates(self) -> Set[datetime.date]: return {rate.rate_date for rate in self} def add_rate(self, new_rate: ExchangeRate) -> None: self._rate_index[new_rate.descriptor] = new_rate def __iter__(self): return iter(list(self._rate_index.values())) def __len__(self): return len(self._rate_index) def __contains__(self, rate) -> bool: if not isinstance(rate, ExchangeRate): raise TypeError("ExchangeRates can only hold Rates.") if rate.descriptor in self._rate_index: return True return False def is_rate_present(self, rate: ExchangeRate) -> bool: if rate.descriptor in self._rate_index: return True return False def __getitem__(self, rate_descriptor) -> ExchangeRate: return self._rate_index[rate_descriptor] def add_equal_rates(rates: ExchangeRates, overwrite: bool = False) -> ExchangeRates: present_currencies = rates.present_currencies present_dates = rates.present_dates for date in present_dates: for currency in present_currencies: new_rate = ExchangeRate( from_currency=currency, to_currency=currency, rate=MoneyAmount(1, currency), rate_date=date, ) if new_rate in rates and not overwrite: continue rates.add_rate(new_rate) return rates def add_inverse_rates(rates: ExchangeRates) -> ExchangeRates: # Hey, I haven't thought properly what happens here if the inverse rate is # *already* present in the rates set. It's probably going to be fucky. I # would advise only running this against sets where you don't have inverse # rates already present. for rate in rates: inverse_rate = ExchangeRate( from_currency=rate.to_currency, to_currency=rate.from_currency, rate_date=rate.rate_date, rate=f"{1 / rate.amount:.{DEFAULT_MONEY_PRECISION_POSITIONS}f}", ) rates.add_rate(inverse_rate) return rates