diff --git a/oxrlib/commands/historical.py b/oxrlib/commands/historical.py index 0b0b58f39fe892b125bd8b9d6681dfc335212239..47c141a8f8509b3a1e8a5e3bc15b376bb1f23750 100644 --- a/oxrlib/commands/historical.py +++ b/oxrlib/commands/historical.py @@ -55,14 +55,16 @@ class Formatter: class LedgerFormatter(Formatter): - RATE_PREC = 5 + def __init__(self, rate, signed_currencies=(), base_fmt='#,##0.###', + rate_precision=5, denomination=None): + super().__init__(rate, signed_currencies, base_fmt) + self.rate_prec = rate_precision + self.denomination = denomination - def normalize_rate(self, rate, prec=None): - # Return prec nonzero digits of precision, if available. - if prec is None: - prec = self.RATE_PREC + def normalize_rate(self, rate): _, digits, exponent = rate.normalize().as_tuple() - prec -= min(0, exponent + len(digits)) + # Return ``self.rate_prec`` nonzero digits of precision, if available. + prec = self.rate_prec - min(0, exponent + len(digits)) quant_to = '1.{}'.format('0' * prec) try: qrate = rate.quantize(decimal.Decimal(quant_to)) @@ -71,11 +73,11 @@ class LedgerFormatter(Formatter): qrate = rate return qrate.normalize() - def format_rate(self, rate, prec=None): - return str(self.normalize_rate(rate, prec)) + def format_rate(self, rate): + return str(self.normalize_rate(rate)) - def format_ledger_rate(self, rate, curr, prec=None): - nrate = self.normalize_rate(rate, prec) + def format_ledger_rate(self, rate, curr): + nrate = self.normalize_rate(rate) rate_s = self.format_currency(nrate, curr, currency_digits=False) return "{{={0}}} @ {0}".format(rate_s) @@ -85,13 +87,28 @@ class LedgerFormatter(Formatter): return "{} {} {}".format( from_amt, from_curr, self.format_ledger_rate(to_amt, to_curr)) + 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 + else: + rate = self.rate.convert(1, currency, denomination) + return "{} {}".format(amt_s, self.format_ledger_rate(rate, denomination)) + def format_conversion(self, from_amt, from_curr, to_curr): - to_rate = self.rate.convert(1, from_curr, to_curr) to_amt = self.rate.convert(from_amt, from_curr, to_curr) - return "{} {}\n{}".format( - self.format_currency(from_amt, from_curr), - self.format_ledger_rate(to_rate, to_curr), - self.format_currency(to_amt, to_curr), + return "{}\n{}".format( + self.format_denominated_rate(from_amt, from_curr, to_curr), + self.format_denominated_rate(to_amt, to_curr, None), ) @@ -102,7 +119,8 @@ def run(config, stdout, stderr): if loaders.should_cache(): config.cache.save_rate(rate) if config.args.ledger: - formatter = LedgerFormatter(rate, config.args.signed_currencies) + formatter = LedgerFormatter(rate, config.args.signed_currencies, + denomination=config.args.denomination) else: formatter = Formatter(rate) if not config.args.from_currency: diff --git a/oxrlib/config.py b/oxrlib/config.py index 6df93891031db651ed8f9134fc73c743c649b8e0..e85cb3aa4a02d3dcf00b4bd0665cf9425fa20e9b 100644 --- a/oxrlib/config.py +++ b/oxrlib/config.py @@ -20,6 +20,7 @@ def currency_list(s): class Configuration: DATE_SEPS = frozenset('.-/ ') DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini') + NO_DENOMINATION = object() PREPOSITIONS = frozenset(['in', 'to', 'into']) TODAY = datetime.date.today() @@ -97,6 +98,17 @@ class Configuration: action='store_false', dest='ledger', help="Turn off an earlier --ledger setting", ) + hist_parser.add_argument( + '--denomination', + metavar='CODE', type=currency_code, + help="In Ledger conversion output, always show rates to convert " + "to this currency", + ) + hist_parser.add_argument( + '--no-denomination', + dest='denomination', action='store_const', const=self.NO_DENOMINATION, + help="Turn off an earlier --denomination setting", + ) hist_parser.add_argument( '--signed-currency', '--sign-currency', type=currency_code, action='append', dest='signed_currencies', @@ -132,14 +144,16 @@ class Configuration: return configparser.ConfigParser() def _read_from_conffile(self, argname, sectionname, fallback, convert_to=None, - confname=None, getter='get', unset=None): + confname=None, getter='get', unset=None, + *, convert_fallback=False): if getattr(self.args, argname) is not unset: return elif confname is None: confname = argname get_method = getattr(self.conffile, getter) value = get_method(sectionname, confname, fallback=fallback) - if convert_to is not None: + if (convert_to is not None + and (value is not fallback or convert_fallback)): value = self._convert_or_error(convert_to, value, confname) setattr(self.args, argname, value) @@ -162,7 +176,12 @@ class Configuration: # Don't let the user specify ambiguous dates. self.error("historical data not available from year {}".format(year)) self._read_from_conffile('base', 'Historical', 'USD', currency_code) - self._read_from_conffile('signed_currencies', 'Historical', self.args.base, currency_list) + if self.args.denomination is self.NO_DENOMINATION: + self.args.denomination = None + else: + self._read_from_conffile('denomination', 'Historical', None, currency_code) + self._read_from_conffile('signed_currencies', 'Historical', self.args.base, + currency_list, convert_fallback=True) self._read_from_conffile('ledger', 'Historical', False, getter='getboolean') self.args.to_currency = self.args.base if self.args.word4 and (self.args.word3.lower() in self.PREPOSITIONS): diff --git a/oxrlib_example.ini b/oxrlib_example.ini index 0e56338eb38e1be55ec1b83406f2d0e1817bed83..57fd7cff98cb47d72480a89a3460cb842c1698de 100644 --- a/oxrlib_example.ini +++ b/oxrlib_example.ini @@ -27,6 +27,13 @@ base = USD # Write output in Ledger format. ledger = no +# Denominate Ledger books in this currency. +# Ledger-formatted conversions will always show a rate to convert to this +# currency, even when converting between two other currencies. +# If not specified, output will show the rate for the currency you're +# converting to. +denomination = USD + # Use signs for these currencies in Ledger output. # If not specified, defaults to the base currency. signed_currencies = USD, EUR diff --git a/setup.py b/setup.py index 5e66678986187f9d7180313ce3f5e7d8933d60ba..b48c3fe45cdce62ff82cb422993adbae76c2f840 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup setup( name='oxrlib', description="Library to query the Open Exchange Rates (OXR) API", - version='1.3', + version='1.4', author='Brett Smith', author_email='brettcsmith@brettcsmith.org', license='GNU AGPLv3+', diff --git a/tests/test_historical.py b/tests/test_historical.py index 7eaf13a1ca71f97aeb12a5b904af0b3c743a3c92..d68b803ac228c6e80929be2e22316942558bbf8a 100644 --- a/tests/test_historical.py +++ b/tests/test_historical.py @@ -49,6 +49,7 @@ def build_config( to_currency=None, ledger=False, signed_currencies=None, + denomination=None, base='USD', ): return FakeConfig(responder, { @@ -59,6 +60,7 @@ def build_config( 'to_currency': base if to_currency is None else to_currency, 'ledger': ledger, 'signed_currencies': [base] if signed_currencies is None else signed_currencies, + 'denomination': denomination, }) def lines_from_run(config, output): @@ -117,3 +119,30 @@ def test_signed_currencies(historical1_responder, output): assert next(lines) == '1 AED {=0.2723 USD} @ 0.2723 USD\n' assert next(lines) == '1 USD {=3.67246 AED} @ 3.67246 AED\n' assert next(lines, None) is None + +def test_denomination(historical1_responder, output): + config = build_config(historical1_responder, from_currency='ANG', + to_currency='AED', amount=10, + ledger=True, denomination='USD') + lines = lines_from_run(config, output) + assert next(lines) == '10.00 ANG {=$0.55866} @ $0.55866\n' + assert next(lines) == '20.52 AED {=$0.2723} @ $0.2723\n' + assert next(lines, None) is None + +def test_redundant_denomination(historical1_responder, output): + config = build_config(historical1_responder, from_currency='ANG', + to_currency='USD', amount=10, + ledger=True, denomination='USD') + lines = lines_from_run(config, output) + assert next(lines) == '10.00 ANG {=$0.55866} @ $0.55866\n' + assert next(lines) == '$5.59\n' + assert next(lines, None) is None + +def test_from_denomination(historical1_responder, output): + config = build_config(historical1_responder, from_currency='USD', + to_currency='ALL', amount=10, + ledger=True, denomination='USD') + lines = lines_from_run(config, output) + assert next(lines) == '$10.00\n' + assert next(lines) == '1,445 ALL {=$0.006919} @ $0.006919\n' + assert next(lines, None) is None