From 385a492ae735987c813df2e57ce7b0bbedbcc887 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Thu, 8 Jun 2017 09:52:47 -0400 Subject: [PATCH] config: More flexible date parsing. * Accept partial dates, filling in the current year and month as needed. * Accept more separators. --- oxrlib/config.py | 42 ++++++++++++++++++++++++++++++------- setup.py | 2 +- tests/test_Configuration.py | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/oxrlib/config.py b/oxrlib/config.py index 9a35c4f..b4974a1 100644 --- a/oxrlib/config.py +++ b/oxrlib/config.py @@ -17,14 +17,11 @@ def currency_code(s): def currency_list(s): 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: + DATE_SEPS = frozenset('.-/ ') DEFAULT_CONFIG_PATH = pathlib.Path(HOME_PATH, '.config', 'oxrlib.ini') PREPOSITIONS = frozenset(['in', 'to', 'into']) + TODAY = datetime.date.today() def __init__(self, arglist): argparser = self._build_argparser() @@ -50,6 +47,30 @@ class Configuration: else: 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): prog_parser = argparse.ArgumentParser() prog_parser.add_argument( @@ -61,7 +82,7 @@ class Configuration: hist_parser = subparsers.add_parser( '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", ) hist_parser.set_defaults( @@ -94,8 +115,9 @@ class Configuration: ) hist_parser.add_argument( 'date', - type=date_from('%Y-%m-%d'), - help="Use rates from this date, in YYYY-MM-DD format" + type=self._date_from_s, + 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( 'word1', nargs='?', metavar='first code', @@ -144,6 +166,10 @@ class Configuration: self.error(': '.join(errmsg)) 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('signed_currencies', 'Historical', self.args.base, currency_list) self._read_from_conffile('ledger', 'Historical', False, getter='getboolean') diff --git a/setup.py b/setup.py index 94c0d60..5e66678 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.2', + version='1.3', author='Brett Smith', author_email='brettcsmith@brettcsmith.org', license='GNU AGPLv3+', diff --git a/tests/test_Configuration.py b/tests/test_Configuration.py index 0c94f1c..0f5e94d 100644 --- a/tests/test_Configuration.py +++ b/tests/test_Configuration.py @@ -1,3 +1,4 @@ +import datetime import decimal import os @@ -78,3 +79,39 @@ def test_historical_argparsing_failure(arglist, any_date): pass else: 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)