Merge pull request #2528 from discopatrick/query-datetime-parser

Query datetime parser
This commit is contained in:
Adrian Sampson 2017-06-10 20:57:29 -04:00
commit 389aed8d5d
3 changed files with 121 additions and 11 deletions

View file

@ -533,12 +533,20 @@ class Period(object):
instants of time during January 2014.
"""
precisions = ('year', 'month', 'day')
date_formats = ('%Y', '%Y-%m', '%Y-%m-%d')
precisions = ('year', 'month', 'day', 'hour', 'minute', 'second')
date_formats = (
('%Y',), # year
('%Y-%m',), # month
('%Y-%m-%d',), # day
('%Y-%m-%dT%H', '%Y-%m-%d %H'), # hour
('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute
('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second
)
def __init__(self, date, precision):
"""Create a period with the given date (a `datetime` object) and
precision (a string, one of "year", "month", or "day").
precision (a string, one of "year", "month", "day", "hour", "minute",
or "second").
"""
if precision not in Period.precisions:
raise ValueError(u'Invalid precision {0}'.format(precision))
@ -551,16 +559,21 @@ class Period(object):
string is empty, or raise an InvalidQueryArgumentValueError if
the string could not be parsed to a date.
"""
def find_date_and_format(string):
for ord, format in enumerate(cls.date_formats):
for format_option in format:
try:
date = datetime.strptime(string, format_option)
return date, ord
except ValueError:
# Parsing failed.
pass
return (None, None)
if not string:
return None
date = None
for ordinal, date_format in enumerate(cls.date_formats):
try:
date = datetime.strptime(string, date_format)
break
except ValueError:
# Parsing failed.
pass
date, ordinal = find_date_and_format(string)
if date is None:
raise InvalidQueryArgumentValueError(string,
'a valid datetime string')
@ -582,6 +595,12 @@ class Period(object):
return date.replace(year=date.year + 1, month=1)
elif 'day' == precision:
return date + timedelta(days=1)
elif 'hour' == precision:
return date + timedelta(hours=1)
elif 'minute' == precision:
return date + timedelta(minutes=1)
elif 'second' == precision:
return date + timedelta(seconds=1)
else:
raise ValueError(u'unhandled precision {0}'.format(precision))

View file

@ -188,6 +188,33 @@ Find all items with a file modification time between 2008-12-01 and
$ beet ls 'mtime:2008-12-01..2008-12-02'
You can also add an optional time value to date queries, specifying hours,
minutes, and seconds.
Times are separated from dates by a space, an uppercase 'T' or a lowercase
't', for example: ``2008-12-01T23:59:59``. If you specify a time, then the
date must contain a year, month, and day. The minutes and seconds are
optional.
Here is an example that finds all items added on 2008-12-01 at or after 22:00
but before 23:00::
$ beet ls 'added:2008-12-01T22'
Find all items added on or after 2008-12-01 22:45::
$ beet ls 'added:2008-12-01T22:45..'
Find all items added on 2008-12-01, at or after 22:45:20 but before 22:45:41::
$ beet ls 'added:2008-12-01T22:45:20..2008-12-01T22:45:40'
Examples of each time format::
$ beet ls 'added:2008-12-01T22:45:20'
$ beet ls 'added:2008-12-01t22:45:20'
$ beet ls 'added:2008-12-01 22:45:20'
.. _not_query:
Query Term Negation

View file

@ -58,6 +58,51 @@ class DateIntervalTest(unittest.TestCase):
self.assertExcludes('1999-12..2000-02', '1999-11-30T23:59:59')
self.assertExcludes('1999-12..2000-02', '2000-03-01T00:00:00')
def test_hour_precision_intervals(self):
# test with 'T' separator
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T11:59:59')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T12:00:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T12:30:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T13:30:00')
self.assertContains('2000-01-01T12..2000-01-01T13',
'2000-01-01T13:59:59')
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T14:00:00')
self.assertExcludes('2000-01-01T12..2000-01-01T13',
'2000-01-01T14:30:00')
# test non-range query
self.assertContains('2008-12-01T22',
'2008-12-01T22:30:00')
self.assertExcludes('2008-12-01T22',
'2008-12-01T23:30:00')
def test_minute_precision_intervals(self):
self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:29:59')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:30:00')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:30:30')
self.assertContains('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:31:59')
self.assertExcludes('2000-01-01T12:30..2000-01-01T12:31',
'2000-01-01T12:32:00')
def test_second_precision_intervals(self):
self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:49')
self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:50')
self.assertContains('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:55')
self.assertExcludes('2000-01-01T12:30:50..2000-01-01T12:30:55',
'2000-01-01T12:30:56')
def test_unbounded_endpoints(self):
self.assertContains('..', date=datetime.max)
self.assertContains('..', date=datetime.min)
@ -140,6 +185,25 @@ class DateQueryConstructTest(unittest.TestCase):
with self.assertRaises(InvalidQueryArgumentValueError):
DateQuery('added', q)
def test_datetime_uppercase_t_separator(self):
date_query = DateQuery('added', '2000-01-01T12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))
def test_datetime_lowercase_t_separator(self):
date_query = DateQuery('added', '2000-01-01t12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))
def test_datetime_space_separator(self):
date_query = DateQuery('added', '2000-01-01 12')
self.assertEqual(date_query.interval.start, datetime(2000, 1, 1, 12))
self.assertEqual(date_query.interval.end, datetime(2000, 1, 1, 13))
def test_datetime_invalid_separator(self):
with self.assertRaises(InvalidQueryArgumentValueError):
DateQuery('added', '2000-01-01x12')
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)