historical: Add a setting to denominate Ledger conversions.
This makes conversion output easier to add to ledgers directly.
This commit is contained in:
parent
992c91fc90
commit
55f5833aa0
5 changed files with 93 additions and 20 deletions
|
@ -55,14 +55,16 @@ class Formatter:
|
||||||
|
|
||||||
|
|
||||||
class LedgerFormatter(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):
|
def normalize_rate(self, rate):
|
||||||
# Return prec nonzero digits of precision, if available.
|
|
||||||
if prec is None:
|
|
||||||
prec = self.RATE_PREC
|
|
||||||
_, digits, exponent = rate.normalize().as_tuple()
|
_, 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)
|
quant_to = '1.{}'.format('0' * prec)
|
||||||
try:
|
try:
|
||||||
qrate = rate.quantize(decimal.Decimal(quant_to))
|
qrate = rate.quantize(decimal.Decimal(quant_to))
|
||||||
|
@ -71,11 +73,11 @@ class LedgerFormatter(Formatter):
|
||||||
qrate = rate
|
qrate = rate
|
||||||
return qrate.normalize()
|
return qrate.normalize()
|
||||||
|
|
||||||
def format_rate(self, rate, prec=None):
|
def format_rate(self, rate):
|
||||||
return str(self.normalize_rate(rate, prec))
|
return str(self.normalize_rate(rate))
|
||||||
|
|
||||||
def format_ledger_rate(self, rate, curr, prec=None):
|
def format_ledger_rate(self, rate, curr):
|
||||||
nrate = self.normalize_rate(rate, prec)
|
nrate = self.normalize_rate(rate)
|
||||||
rate_s = self.format_currency(nrate, curr, currency_digits=False)
|
rate_s = self.format_currency(nrate, curr, currency_digits=False)
|
||||||
return "{{={0}}} @ {0}".format(rate_s)
|
return "{{={0}}} @ {0}".format(rate_s)
|
||||||
|
|
||||||
|
@ -85,13 +87,28 @@ class LedgerFormatter(Formatter):
|
||||||
return "{} {} {}".format(
|
return "{} {} {}".format(
|
||||||
from_amt, from_curr, self.format_ledger_rate(to_amt, to_curr))
|
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):
|
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)
|
to_amt = self.rate.convert(from_amt, from_curr, to_curr)
|
||||||
return "{} {}\n{}".format(
|
return "{}\n{}".format(
|
||||||
self.format_currency(from_amt, from_curr),
|
self.format_denominated_rate(from_amt, from_curr, to_curr),
|
||||||
self.format_ledger_rate(to_rate, to_curr),
|
self.format_denominated_rate(to_amt, to_curr, None),
|
||||||
self.format_currency(to_amt, to_curr),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -102,7 +119,8 @@ def run(config, stdout, stderr):
|
||||||
if loaders.should_cache():
|
if loaders.should_cache():
|
||||||
config.cache.save_rate(rate)
|
config.cache.save_rate(rate)
|
||||||
if config.args.ledger:
|
if config.args.ledger:
|
||||||
formatter = LedgerFormatter(rate, config.args.signed_currencies)
|
formatter = LedgerFormatter(rate, config.args.signed_currencies,
|
||||||
|
denomination=config.args.denomination)
|
||||||
else:
|
else:
|
||||||
formatter = Formatter(rate)
|
formatter = Formatter(rate)
|
||||||
if not config.args.from_currency:
|
if not config.args.from_currency:
|
||||||
|
|
|
@ -20,6 +20,7 @@ def currency_list(s):
|
||||||
class Configuration:
|
class Configuration:
|
||||||
DATE_SEPS = frozenset('.-/ ')
|
DATE_SEPS = frozenset('.-/ ')
|
||||||
DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini')
|
DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini')
|
||||||
|
NO_DENOMINATION = object()
|
||||||
PREPOSITIONS = frozenset(['in', 'to', 'into'])
|
PREPOSITIONS = frozenset(['in', 'to', 'into'])
|
||||||
TODAY = datetime.date.today()
|
TODAY = datetime.date.today()
|
||||||
|
|
||||||
|
@ -97,6 +98,17 @@ class Configuration:
|
||||||
action='store_false', dest='ledger',
|
action='store_false', dest='ledger',
|
||||||
help="Turn off an earlier --ledger setting",
|
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(
|
hist_parser.add_argument(
|
||||||
'--signed-currency', '--sign-currency',
|
'--signed-currency', '--sign-currency',
|
||||||
type=currency_code, action='append', dest='signed_currencies',
|
type=currency_code, action='append', dest='signed_currencies',
|
||||||
|
@ -132,14 +144,16 @@ class Configuration:
|
||||||
return configparser.ConfigParser()
|
return configparser.ConfigParser()
|
||||||
|
|
||||||
def _read_from_conffile(self, argname, sectionname, fallback, convert_to=None,
|
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:
|
if getattr(self.args, argname) is not unset:
|
||||||
return
|
return
|
||||||
elif confname is None:
|
elif confname is None:
|
||||||
confname = argname
|
confname = argname
|
||||||
get_method = getattr(self.conffile, getter)
|
get_method = getattr(self.conffile, getter)
|
||||||
value = get_method(sectionname, confname, fallback=fallback)
|
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)
|
value = self._convert_or_error(convert_to, value, confname)
|
||||||
setattr(self.args, argname, value)
|
setattr(self.args, argname, value)
|
||||||
|
|
||||||
|
@ -162,7 +176,12 @@ class Configuration:
|
||||||
# Don't let the user specify ambiguous dates.
|
# Don't let the user specify ambiguous dates.
|
||||||
self.error("historical data not available from year {}".format(year))
|
self.error("historical data not available from year {}".format(year))
|
||||||
self._read_from_conffile('base', 'Historical', 'USD', currency_code)
|
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._read_from_conffile('ledger', 'Historical', False, getter='getboolean')
|
||||||
self.args.to_currency = self.args.base
|
self.args.to_currency = self.args.base
|
||||||
if self.args.word4 and (self.args.word3.lower() in self.PREPOSITIONS):
|
if self.args.word4 and (self.args.word3.lower() in self.PREPOSITIONS):
|
||||||
|
|
|
@ -27,6 +27,13 @@ base = USD
|
||||||
# Write output in Ledger format.
|
# Write output in Ledger format.
|
||||||
ledger = no
|
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.
|
# Use signs for these currencies in Ledger output.
|
||||||
# If not specified, defaults to the base currency.
|
# If not specified, defaults to the base currency.
|
||||||
signed_currencies = USD, EUR
|
signed_currencies = USD, EUR
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -5,7 +5,7 @@ from setuptools import setup
|
||||||
setup(
|
setup(
|
||||||
name='oxrlib',
|
name='oxrlib',
|
||||||
description="Library to query the Open Exchange Rates (OXR) API",
|
description="Library to query the Open Exchange Rates (OXR) API",
|
||||||
version='1.3',
|
version='1.4',
|
||||||
author='Brett Smith',
|
author='Brett Smith',
|
||||||
author_email='brettcsmith@brettcsmith.org',
|
author_email='brettcsmith@brettcsmith.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
|
@ -49,6 +49,7 @@ def build_config(
|
||||||
to_currency=None,
|
to_currency=None,
|
||||||
ledger=False,
|
ledger=False,
|
||||||
signed_currencies=None,
|
signed_currencies=None,
|
||||||
|
denomination=None,
|
||||||
base='USD',
|
base='USD',
|
||||||
):
|
):
|
||||||
return FakeConfig(responder, {
|
return FakeConfig(responder, {
|
||||||
|
@ -59,6 +60,7 @@ def build_config(
|
||||||
'to_currency': base if to_currency is None else to_currency,
|
'to_currency': base if to_currency is None else to_currency,
|
||||||
'ledger': ledger,
|
'ledger': ledger,
|
||||||
'signed_currencies': [base] if signed_currencies is None else signed_currencies,
|
'signed_currencies': [base] if signed_currencies is None else signed_currencies,
|
||||||
|
'denomination': denomination,
|
||||||
})
|
})
|
||||||
|
|
||||||
def lines_from_run(config, output):
|
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 AED {=0.2723 USD} @ 0.2723 USD\n'
|
||||||
assert next(lines) == '1 USD {=3.67246 AED} @ 3.67246 AED\n'
|
assert next(lines) == '1 USD {=3.67246 AED} @ 3.67246 AED\n'
|
||||||
assert next(lines, None) is None
|
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
|
||||||
|
|
Loading…
Reference in a new issue