config: More flexible date parsing.
* Accept partial dates, filling in the current year and month as needed. * Accept more separators.
This commit is contained in:
parent
17ff9a8b71
commit
385a492ae7
3 changed files with 72 additions and 9 deletions
|
@ -17,14 +17,11 @@ def currency_code(s):
|
||||||
def currency_list(s):
|
def currency_list(s):
|
||||||
return [currency_code(code.strip()) for code in s.split(',')]
|
return [currency_code(code.strip()) for code in s.split(',')]
|
||||||
|
|
||||||
def date_from(fmt_s):
|
|
||||||
def date_from_fmt(s):
|
|
||||||
return datetime.datetime.strptime(s, fmt_s).date()
|
|
||||||
return date_from_fmt
|
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
|
DATE_SEPS = frozenset('.-/ ')
|
||||||
DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini')
|
DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini')
|
||||||
PREPOSITIONS = frozenset(['in', 'to', 'into'])
|
PREPOSITIONS = frozenset(['in', 'to', 'into'])
|
||||||
|
TODAY = datetime.date.today()
|
||||||
|
|
||||||
def __init__(self, arglist):
|
def __init__(self, arglist):
|
||||||
argparser = self._build_argparser()
|
argparser = self._build_argparser()
|
||||||
|
@ -50,6 +47,30 @@ class Configuration:
|
||||||
else:
|
else:
|
||||||
post_hook()
|
post_hook()
|
||||||
|
|
||||||
|
def _date_from_s(self, s):
|
||||||
|
number_digits = [[]]
|
||||||
|
seen_seps = set()
|
||||||
|
bad_c = False
|
||||||
|
for c in s.strip():
|
||||||
|
if c.isdigit():
|
||||||
|
number_digits[-1].append(c)
|
||||||
|
elif c in self.DATE_SEPS:
|
||||||
|
seen_seps.add(c)
|
||||||
|
number_digits.append([])
|
||||||
|
else:
|
||||||
|
bad_c = True
|
||||||
|
numbers = [int(''.join(digit_list), 10) for digit_list in number_digits]
|
||||||
|
if bad_c or (len(numbers) > 3) or (len(seen_seps) > 1):
|
||||||
|
raise ValueError("can't parse date from {!r}".format(s))
|
||||||
|
replacements = {}
|
||||||
|
try:
|
||||||
|
replacements['day'] = numbers[-1]
|
||||||
|
replacements['month'] = numbers[-2]
|
||||||
|
replacements['year'] = numbers[-3]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return self.TODAY.replace(**replacements)
|
||||||
|
|
||||||
def _build_argparser(self):
|
def _build_argparser(self):
|
||||||
prog_parser = argparse.ArgumentParser()
|
prog_parser = argparse.ArgumentParser()
|
||||||
prog_parser.add_argument(
|
prog_parser.add_argument(
|
||||||
|
@ -61,7 +82,7 @@ class Configuration:
|
||||||
|
|
||||||
hist_parser = subparsers.add_parser(
|
hist_parser = subparsers.add_parser(
|
||||||
'historical', aliases=['hist'],
|
'historical', aliases=['hist'],
|
||||||
usage='%(prog)s YYYY-MM-DD [[amount] code] [[in] code]',
|
usage='%(prog)s [[YYYY-]MM-]DD [[amount] code] [[in] code]',
|
||||||
help="Show a currency conversion or rate from a past date",
|
help="Show a currency conversion or rate from a past date",
|
||||||
)
|
)
|
||||||
hist_parser.set_defaults(
|
hist_parser.set_defaults(
|
||||||
|
@ -94,8 +115,9 @@ class Configuration:
|
||||||
)
|
)
|
||||||
hist_parser.add_argument(
|
hist_parser.add_argument(
|
||||||
'date',
|
'date',
|
||||||
type=date_from('%Y-%m-%d'),
|
type=self._date_from_s,
|
||||||
help="Use rates from this date, in YYYY-MM-DD format"
|
help="Use rates from this date, in YYYY-MM-DD format. "
|
||||||
|
"If you omit the year or month, it fills in the current year/month."
|
||||||
)
|
)
|
||||||
hist_parser.add_argument(
|
hist_parser.add_argument(
|
||||||
'word1', nargs='?', metavar='first code',
|
'word1', nargs='?', metavar='first code',
|
||||||
|
@ -144,6 +166,10 @@ class Configuration:
|
||||||
self.error(': '.join(errmsg))
|
self.error(': '.join(errmsg))
|
||||||
|
|
||||||
def _post_hook_historical(self):
|
def _post_hook_historical(self):
|
||||||
|
year = self.args.date.year
|
||||||
|
if year < 100:
|
||||||
|
# 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('base', 'Historical', 'USD', currency_code)
|
||||||
self._read_from_conffile('signed_currencies', 'Historical', self.args.base, currency_list)
|
self._read_from_conffile('signed_currencies', 'Historical', self.args.base, currency_list)
|
||||||
self._read_from_conffile('ledger', 'Historical', False, getter='getboolean')
|
self._read_from_conffile('ledger', 'Historical', False, getter='getboolean')
|
||||||
|
|
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.2',
|
version='1.3',
|
||||||
author='Brett Smith',
|
author='Brett Smith',
|
||||||
author_email='brettcsmith@brettcsmith.org',
|
author_email='brettcsmith@brettcsmith.org',
|
||||||
license='GNU AGPLv3+',
|
license='GNU AGPLv3+',
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -78,3 +79,39 @@ def test_historical_argparsing_failure(arglist, any_date):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
assert not vars(config.args), "bad arglist succeeded"
|
assert not vars(config.args), "bad arglist succeeded"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('date_s,expect_year,expect_month,expect_day', [
|
||||||
|
('5', 1965, 4, 5),
|
||||||
|
('05', 1965, 4, 5),
|
||||||
|
('3-6', 1965, 3, 6),
|
||||||
|
('5.10', 1965, 5, 10),
|
||||||
|
('06-09', 1965, 6, 9),
|
||||||
|
('917/12/12', 917, 12, 12),
|
||||||
|
('2017-11-1', 2017, 11, 1),
|
||||||
|
])
|
||||||
|
def test_good_date_parsing(date_s, expect_year, expect_month, expect_day):
|
||||||
|
oxrlib.config.Configuration.TODAY = datetime.date(1965, 4, 3)
|
||||||
|
config = config_from(os.devnull, ['historical', date_s])
|
||||||
|
actual_date = config.args.date
|
||||||
|
assert actual_date.year == expect_year
|
||||||
|
assert actual_date.month == expect_month
|
||||||
|
assert actual_date.day == expect_day
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('date_s', [
|
||||||
|
'99',
|
||||||
|
'8-88',
|
||||||
|
'77-7',
|
||||||
|
'0xf-1-2',
|
||||||
|
'0b1-3-4',
|
||||||
|
'2017/5.9',
|
||||||
|
'2018-6/10',
|
||||||
|
'1-2-3-4',
|
||||||
|
'12/11/10',
|
||||||
|
])
|
||||||
|
def test_bad_date_parsing(date_s):
|
||||||
|
try:
|
||||||
|
config = config_from(os.devnull, ['historical', date_s])
|
||||||
|
except SystemExit:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
assert not config.args.date, "date parsed from {!r}".format(date_s)
|
||||||
|
|
Loading…
Reference in a new issue