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:
Brett Smith 2017-06-08 09:52:47 -04:00
parent 17ff9a8b71
commit 385a492ae7
3 changed files with 72 additions and 9 deletions

View file

@ -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')

View file

@ -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+',

View file

@ -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)