mirror of
https://github.com/beetbox/beets.git
synced 2026-02-27 09:41:51 +01:00
Merge pull request #2598 from beetbox/relativedate
Relative date queries (continuation of #2418)
This commit is contained in:
commit
d31a48345f
4 changed files with 145 additions and 4 deletions
|
|
@ -562,6 +562,9 @@ class Period(object):
|
|||
('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute
|
||||
('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second
|
||||
)
|
||||
relative_units = {'y': 365, 'm': 30, 'w': 7, 'd': 1}
|
||||
relative_re = '(?P<sign>[+|-]?)(?P<quantity>[0-9]+)' + \
|
||||
'(?P<timespan>[y|m|w|d])'
|
||||
|
||||
def __init__(self, date, precision):
|
||||
"""Create a period with the given date (a `datetime` object) and
|
||||
|
|
@ -575,9 +578,20 @@ class Period(object):
|
|||
|
||||
@classmethod
|
||||
def parse(cls, string):
|
||||
"""Parse a date and return a `Period` object, or `None` if the
|
||||
"""Parse a date and return a `Period` object or `None` if the
|
||||
string is empty, or raise an InvalidQueryArgumentValueError if
|
||||
the string could not be parsed to a date.
|
||||
the string cannot be parsed to a date.
|
||||
|
||||
The date may be absolute or relative. Absolute dates look like
|
||||
`YYYY`, or `YYYY-MM-DD`, or `YYYY-MM-DD HH:MM:SS`, etc. Relative
|
||||
dates have three parts:
|
||||
|
||||
- Optionally, a ``+`` or ``-`` sign indicating the future or the
|
||||
past. The default is the future.
|
||||
- A number: how much to add or subtract.
|
||||
- A letter indicating the unit: days, weeks, months or years
|
||||
(``d``, ``w``, ``m`` or ``y``). A "month" is exactly 30 days
|
||||
and a "year" is exactly 365 days.
|
||||
"""
|
||||
|
||||
def find_date_and_format(string):
|
||||
|
|
@ -593,10 +607,27 @@ class Period(object):
|
|||
|
||||
if not string:
|
||||
return None
|
||||
|
||||
# Check for a relative date.
|
||||
match_dq = re.match(cls.relative_re, string)
|
||||
if match_dq:
|
||||
sign = match_dq.group('sign')
|
||||
quantity = match_dq.group('quantity')
|
||||
timespan = match_dq.group('timespan')
|
||||
|
||||
# Add or subtract the given amount of time from the current
|
||||
# date.
|
||||
multiplier = -1 if sign == '-' else 1
|
||||
days = cls.relative_units[timespan]
|
||||
date = datetime.now() + \
|
||||
timedelta(days=int(quantity) * days) * multiplier
|
||||
return cls(date, cls.precisions[5])
|
||||
|
||||
# Check for an absolute date.
|
||||
date, ordinal = find_date_and_format(string)
|
||||
if date is None:
|
||||
raise InvalidQueryArgumentValueError(string,
|
||||
'a valid datetime string')
|
||||
'a valid date/time string')
|
||||
precision = cls.precisions[ordinal]
|
||||
return cls(date, precision)
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ Features:
|
|||
* :ref:`Date queries <datequery>` can now include times, so you can filter
|
||||
your music down to the second. Thanks to :user:`discopatrick`. :bug:`2506`
|
||||
:bug:`2528`
|
||||
* :ref:`Date queries <datequery>` can also be *relative*. You can say
|
||||
``added:-1w..`` to match music added in the last week, for example. Thanks
|
||||
to :user:`euri10`. :bug:`2598`
|
||||
* A new :doc:`/plugins/gmusic` lets you interact with your Google Play Music
|
||||
library. Thanks to :user:`tigranl`. :bug:`2553` :bug:`2586`
|
||||
* :doc:`/plugins/replaygain`: We now keep R128 data in separate tags from
|
||||
|
|
|
|||
|
|
@ -217,6 +217,25 @@ queries do the same thing::
|
|||
$ beet ls 'added:2008-12-01t22:45:20'
|
||||
$ beet ls 'added:2008-12-01 22:45:20'
|
||||
|
||||
You can also use *relative* dates. For example, ``-3w`` means three weeks ago,
|
||||
and ``+4d`` means four days in the future. A relative date has three parts:
|
||||
|
||||
- Either ``+`` or ``-``, to indicate the past or the future. The sign is
|
||||
optional; if you leave this off, it defaults to the future.
|
||||
- A number.
|
||||
- A letter indicating the unit: ``d``, ``w``, ``m`` or ``y``, meaning days,
|
||||
weeks, months or years. (A "month" is always 30 days and a "year" is always
|
||||
365 days.)
|
||||
|
||||
Here's an example that finds all the albums added since last week::
|
||||
|
||||
$ beet ls -a 'added:-1w..'
|
||||
|
||||
And here's an example that lists items added in a two-week period starting
|
||||
four weeks ago::
|
||||
|
||||
$ beet ls 'added:-6w..-4w'
|
||||
|
||||
.. _not_query:
|
||||
|
||||
Query Term Negation
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
from test import _common
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
import unittest
|
||||
import time
|
||||
from beets.dbcore.query import _parse_periods, DateInterval, DateQuery,\
|
||||
|
|
@ -29,6 +29,10 @@ def _date(string):
|
|||
return datetime.strptime(string, '%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
def _datepattern(datetimedate):
|
||||
return datetimedate.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
class DateIntervalTest(unittest.TestCase):
|
||||
def test_year_precision_intervals(self):
|
||||
self.assertContains('2000..2001', '2000-01-01T00:00:00')
|
||||
|
|
@ -44,6 +48,9 @@ class DateIntervalTest(unittest.TestCase):
|
|||
self.assertContains('..2001', '2001-12-31T23:59:59')
|
||||
self.assertExcludes('..2001', '2002-01-01T00:00:00')
|
||||
|
||||
self.assertContains('-1d..1d', _datepattern(datetime.now()))
|
||||
self.assertExcludes('-2d..-1d', _datepattern(datetime.now()))
|
||||
|
||||
def test_day_precision_intervals(self):
|
||||
self.assertContains('2000-06-20..2000-06-20', '2000-06-20T00:00:00')
|
||||
self.assertContains('2000-06-20..2000-06-20', '2000-06-20T10:20:30')
|
||||
|
|
@ -161,6 +168,87 @@ class DateQueryTest(_common.LibTestCase):
|
|||
self.assertEqual(len(matched), 0)
|
||||
|
||||
|
||||
class DateQueryTestRelative(_common.LibTestCase):
|
||||
def setUp(self):
|
||||
super(DateQueryTestRelative, self).setUp()
|
||||
self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M'))
|
||||
self.i.store()
|
||||
|
||||
def test_single_month_match_fast(self):
|
||||
query = DateQuery('added', datetime.now().strftime('%Y-%m'))
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 1)
|
||||
|
||||
def test_single_month_nonmatch_fast(self):
|
||||
query = DateQuery('added', (datetime.now() + timedelta(days=30))
|
||||
.strftime('%Y-%m'))
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 0)
|
||||
|
||||
def test_single_month_match_slow(self):
|
||||
query = DateQuery('added', datetime.now().strftime('%Y-%m'))
|
||||
self.assertTrue(query.match(self.i))
|
||||
|
||||
def test_single_month_nonmatch_slow(self):
|
||||
query = DateQuery('added', (datetime.now() + timedelta(days=30))
|
||||
.strftime('%Y-%m'))
|
||||
self.assertFalse(query.match(self.i))
|
||||
|
||||
def test_single_day_match_fast(self):
|
||||
query = DateQuery('added', datetime.now().strftime('%Y-%m-%d'))
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 1)
|
||||
|
||||
def test_single_day_nonmatch_fast(self):
|
||||
query = DateQuery('added', (datetime.now() + timedelta(days=1))
|
||||
.strftime('%Y-%m-%d'))
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 0)
|
||||
|
||||
|
||||
class DateQueryTestRelativeMore(_common.LibTestCase):
|
||||
def setUp(self):
|
||||
super(DateQueryTestRelativeMore, self).setUp()
|
||||
self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M'))
|
||||
self.i.store()
|
||||
|
||||
def test_relative(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '-4' + timespan + '..+4' + timespan)
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 1)
|
||||
|
||||
def test_relative_fail(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '-2' + timespan + '..-1' + timespan)
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 0)
|
||||
|
||||
def test_start_relative(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '-4' + timespan + '..')
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 1)
|
||||
|
||||
def test_start_relative_fail(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '4' + timespan + '..')
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 0)
|
||||
|
||||
def test_end_relative(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '..+4' + timespan)
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 1)
|
||||
|
||||
def test_end_relative_fail(self):
|
||||
for timespan in ['d', 'w', 'm', 'y']:
|
||||
query = DateQuery('added', '..-4' + timespan)
|
||||
matched = self.lib.items(query)
|
||||
self.assertEqual(len(matched), 0)
|
||||
|
||||
|
||||
class DateQueryConstructTest(unittest.TestCase):
|
||||
def test_long_numbers(self):
|
||||
with self.assertRaises(InvalidQueryArgumentValueError):
|
||||
|
|
|
|||
Loading…
Reference in a new issue