import decimal import itertools import operator import babel.numbers from .. import rate as oxrrate try: import enum except ImportError: import enum34 as enum class Formatter: def __init__(self, cost_rates, price_rates=None, signed_currencies=(), base_fmt='#,##0.###', rate_precision=5, denomination=None): self.cost_rates = cost_rates self.price_rates = price_rates self.base_fmt = base_fmt self.base_fmt_noprec = base_fmt.rsplit('.', 1)[0] self.signed_currencies = set(code for code in signed_currencies if self.can_sign_currency(code)) self.rate_prec = rate_precision self.denomination = denomination def can_sign_currency(self, code): return False def format_currency(self, amount, code, currency_digits=True): if currency_digits: fmt = self.base_fmt else: fmt = '{}.{}'.format(self.base_fmt_noprec, '#' * -amount.as_tuple().exponent) if code in self.signed_currencies: fmt = '¤' + fmt else: fmt = fmt + ' ¤¤' return babel.numbers.format_currency(amount, code, fmt, currency_digits=currency_digits) def currency_decimal(self, amount, currency): amt_s = babel.numbers.format_currency(amount, currency, '###0.###') return decimal.Decimal(amt_s) def normalize_rate(self, rate, prec=None): return rate def format_rate(self, rate): return "{:g}".format(rate) def format_rate_pair(self, from_curr, to_curr): from_amt = 1 to_amt = self.cost_rates.convert(from_amt, from_curr, to_curr) return "{} {} = {} {}".format( self.format_rate(from_amt), from_curr, self.format_rate(to_amt), to_curr, ) def format_rate_pair_bidir(self, from_curr, to_curr, sep='\n'): return "{}{}{}".format( self.format_rate_pair(from_curr, to_curr), sep, self.format_rate_pair(to_curr, from_curr), ) def format_conversion(self, from_amt, from_curr, to_curr): to_amt = self.cost_rates.convert(from_amt, from_curr, to_curr) return "{} = {}".format( self.format_currency(from_amt, from_curr), self.format_currency(to_amt, to_curr), ) class BeancountFormatter(Formatter): COST_FMT = '{{{}}}' PRICE_FMT = ' @ {}' def price_rate(self, from_amt, from_curr, to_curr): if self.price_rates is None: return None else: return self.price_rates.convert(from_amt, from_curr, to_curr) def normalize_rate(self, rate, prec=None): if prec is None: prec = self.rate_prec _, digits, exponent = rate.normalize().as_tuple() # Return ``prec`` nonzero digits of precision, if available. prec -= min(0, exponent + len(digits)) quant_to = '1.{}'.format('0' * prec) try: qrate = rate.quantize(decimal.Decimal(quant_to)) except decimal.InvalidOperation: # The original rate doesn't have that much precision, so use it raw. qrate = rate return qrate.normalize() def normalize_enough(self, rate, curr, from_amt, to_amt, prec=None): if prec is None: prec = self.rate_prec # Starting from prec, find the least amount of precision to # make sure from_amt converts exactly to to_amt. for try_prec in itertools.count(prec): try_rate = self.normalize_rate(rate, try_prec) got_amt = self.currency_decimal(from_amt * try_rate, curr) # If got_amt == to_amt, this is enough precision to do the # conversion exactly, so we're done. # If try_rate == rate, there's no more precision available, so stop. if (got_amt == to_amt) or (try_rate == rate): break return try_rate def _pretty_rate(self, fmt, rate, curr, from_amt=None, to_amt=None): if to_amt is None: rate = self.normalize_rate(rate) else: rate = self.normalize_enough(rate, curr, from_amt, to_amt) return fmt.format(self.format_currency(rate, curr, currency_digits=False)) def format_rate_pair(self, from_curr, to_curr): from_amt = 1 cost = self.cost_rates.convert(from_amt, from_curr, to_curr) price = self.price_rate(from_amt, from_curr, to_curr) if price is None: price_s = '' else: price_s = self._pretty_rate(self.PRICE_FMT, price, to_curr) return "{} {} {}{}".format( from_amt, from_curr, self._pretty_rate(self.COST_FMT, cost, to_curr), price_s, ) def _denomination_for(self, currency, default): if self.denomination is None: return default elif self.denomination == currency: return None else: return self.denomination def format_denominated_rate(self, amount, currency, default_denomination): denomination = self._denomination_for(currency, default_denomination) amt_s = self.format_currency(amount, currency) if denomination is None: return amt_s cost = self.cost_rates.convert(1, currency, denomination) price = self.price_rate(1, currency, denomination) to_amt = self.currency_decimal(amount * cost, denomination) if price is None: price_s = '' else: price_s = self._pretty_rate( self.PRICE_FMT, price, denomination, amount, to_amt, ) return "{} {}{}".format( amt_s, self._pretty_rate(self.COST_FMT, cost, denomination, amount, to_amt), price_s, ) def format_conversion(self, from_amt, from_curr, to_curr): to_amt = self.cost_rates.convert(from_amt, from_curr, to_curr) return "{}\n{}".format( self.format_denominated_rate(from_amt, from_curr, to_curr), self.format_denominated_rate(to_amt, to_curr, None), ) class LedgerFormatter(BeancountFormatter): COST_FMT = '{{={}}}' def can_sign_currency(self, code): return len(babel.numbers.get_currency_symbol(code)) == 1 def price_rate(self, from_amt, from_curr, to_curr): if self.price_rates is None: rates = self.cost_rates else: rates = self.price_rates return rates.convert(from_amt, from_curr, to_curr) class Formats(enum.Enum): RAW = Formatter LEDGER = LedgerFormatter BEANCOUNT = BeancountFormatter @classmethod def from_arg(cls, s): return cls[s.upper()] def load_rates(config, loaders, date): with loaders.historical(date, config.args.base) as rate_json: rates = oxrrate.Rate.from_json_file(rate_json) if loaders.should_cache(): config.cache.save_rate(rates) return rates def run(config, stdout, stderr): loaders = config.get_loaders() cost_rates = load_rates(config, loaders, config.args.date) if config.args.from_date is None: price_rates = None else: price_rates = load_rates(config, loaders, config.args.from_date) formatter = config.args.output_format.value( cost_rates, price_rates, config.args.signed_currencies, denomination=config.args.denomination, ) if not config.args.from_currency: for from_curr in sorted(cost_rates.rates): print(formatter.format_rate_pair_bidir(from_curr, config.args.to_currency), file=stdout) elif config.args.amount is None: print(formatter.format_rate_pair_bidir(config.args.from_currency, config.args.to_currency), file=stdout) else: print(formatter.format_conversion(config.args.amount, config.args.from_currency, config.args.to_currency), file=stdout)