Files @ 5a73d3d8f8d4
Branch filter:

Location: NPO-Accounting/oxrlib/oxrlib/commands/historical.py

Brett Smith
historical: Move normalize_rate and _pretty_rate to base Formatter.

This reduces the number of method overrides to help readability,
and gets rid of the annoying format_rate/pretty_rate distinction.
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=6, 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):
        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 format_rate(self, rate, curr, fmt='{}', prec=None):
        rate_s = self.format_currency(
            self.normalize_rate(rate, prec),
            curr,
            currency_digits=False,
        )
        return fmt.format(rate_s)

    def format_rate_pair(self, from_curr, to_curr):
        from_amt = decimal.Decimal(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 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.format_rate(price, to_curr, self.PRICE_FMT)
        return "{} {} {}{}".format(
            from_amt,
            from_curr,
            self.format_rate(cost, to_curr, self.COST_FMT),
            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.format_rate(price, denomination, self.PRICE_FMT)
        return "{} {}{}".format(
            amt_s,
            self.format_rate(cost, denomination, self.COST_FMT),
            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()
    date_rates = load_rates(config, loaders, config.args.date)
    if config.args.from_date is None:
        cost_rates = date_rates
        price_rates = None
    else:
        cost_rates = load_rates(config, loaders, config.args.from_date)
        price_rates = date_rates
    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)