diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index e532ed419..51790b9fa 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -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)) diff --git a/docs/reference/query.rst b/docs/reference/query.rst index b4789aa10..70988f026 100644 --- a/docs/reference/query.rst +++ b/docs/reference/query.rst @@ -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 diff --git a/test/test_datequery.py b/test/test_datequery.py index 8ca5680c6..ba8857084 100644 --- a/test/test_datequery.py +++ b/test/test_datequery.py @@ -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__)