Skip to content

Commit 6664b65

Browse files
committed
Merge branch 'relativedate'
Solved conflicts with upstream of new parse classmethod of DateQuery # Conflicts: # beets/dbcore/query.py
2 parents d9c8f97 + d2cd4c0 commit 6664b65

File tree

3 files changed

+148
-4
lines changed

3 files changed

+148
-4
lines changed

beets/dbcore/query.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ class Period(object):
533533
instants of time during January 2014.
534534
"""
535535

536+
536537
precisions = ('year', 'month', 'day', 'hour', 'minute', 'second')
537538
date_formats = (
538539
('%Y',), # year
@@ -542,6 +543,8 @@ class Period(object):
542543
('%Y-%m-%dT%H:%M', '%Y-%m-%d %H:%M'), # minute
543544
('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d %H:%M:%S') # second
544545
)
546+
relative = {'y': 365, 'm': 30, 'w': 7, 'd': 1}
547+
545548

546549
def __init__(self, date, precision):
547550
"""Create a period with the given date (a `datetime` object) and
@@ -555,9 +558,21 @@ def __init__(self, date, precision):
555558

556559
@classmethod
557560
def parse(cls, string):
558-
"""Parse a date and return a `Period` object, or `None` if the
559-
string is empty, or raise an InvalidQueryArgumentValueError if
560-
the string could not be parsed to a date.
561+
"""Parse a date and return a `Period` object or `None` if the
562+
string is empty.
563+
Depending on the string, the date can be absolute or
564+
relative.
565+
An absolute date has to be like one of the date_formats '%Y' or '%Y-%m'
566+
or '%Y-%m-%d'
567+
A relative date consists of three parts:
568+
- a ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+``
569+
sign will add a time quantity to the current date while the ``-`` sign
570+
will do the opposite
571+
- a number follows and indicates the amount to add or substract
572+
- a final letter ends and represents the amount in either days, weeks,
573+
months or years (``d``, ``w``, ``m`` or ``y``)
574+
Please note that this relative calculation makes the assumption of 30
575+
days per month and 365 days per year.
561576
"""
562577

563578
def find_date_and_format(string):
@@ -573,6 +588,20 @@ def find_date_and_format(string):
573588

574589
if not string:
575590
return None
591+
592+
pattern_dq = '(?P<sign>[+|-]?)(?P<quantity>[0-9]+)(?P<timespan>[y|m|w|d])' # noqa: E501
593+
match_dq = re.match(pattern_dq, string)
594+
# test if the string matches the relative date pattern, add the parsed
595+
# quantity to now in that case
596+
if match_dq is not None:
597+
sign = match_dq.group('sign')
598+
quantity = match_dq.group('quantity')
599+
timespan = match_dq.group('timespan')
600+
multiplier = -1 if sign == '-' else 1
601+
days = cls.relative[timespan]
602+
date = datetime.now() + multiplier * timedelta(days=int(quantity) * days)
603+
string = date.strftime(cls.date_formats[5][0])
604+
576605
date, ordinal = find_date_and_format(string)
577606
if date is None:
578607
raise InvalidQueryArgumentValueError(string,
@@ -586,6 +615,8 @@ def open_right_endpoint(self):
586615
"""
587616
precision = self.precision
588617
date = self.date
618+
if 'relative' == self.precision:
619+
return date
589620
if 'year' == self.precision:
590621
return date.replace(year=date.year + 1, month=1)
591622
elif 'month' == precision:
@@ -647,6 +678,7 @@ class DateQuery(FieldQuery):
647678
The value of a date field can be matched against a date interval by
648679
using an ellipsis interval syntax similar to that of NumericQuery.
649680
"""
681+
650682
def __init__(self, field, pattern, fast=True):
651683
super(DateQuery, self).__init__(field, pattern, fast)
652684
start, end = _parse_periods(pattern)
@@ -690,6 +722,7 @@ class DurationQuery(NumericQuery):
690722
Raises InvalidQueryError when the pattern does not represent an int, float
691723
or M:SS time interval.
692724
"""
725+
693726
def _convert(self, s):
694727
"""Convert a M:SS or numeric string to a float.
695728

docs/reference/query.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,29 @@ Dates are written separated by hyphens, like ``year-month-day``, but the month
164164
and day are optional. If you leave out the day, for example, you will get
165165
matches for the whole month.
166166

167+
You can also use relative dates to the current time.
168+
A relative date begins with an ``@``.
169+
It looks like ``@-3w``, ``@2m`` or ``@-4d`` which means the date 3 weeks ago,
170+
the date 2 months from now and the date 4 days ago.
171+
A relative date consists of three parts:
172+
- ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` sign will
173+
add a time quantity to the current date while the ``-`` sign will do the
174+
opposite
175+
- a number follows and indicates the amount to add or substract
176+
- a final letter ends and represents the amount in either days, weeks, months or
177+
years (``d``, ``w``, ``m`` or ``y``)
178+
179+
Please note that this relative calculation makes the assumption of 30 days per
180+
month and 365 days per year.
181+
182+
Here is an example that finds all the albums added between now and last week::
183+
184+
$ beet ls -a 'added:-1w..'
185+
186+
Find all items added in a 2 weeks period 4 weeks ago::
187+
188+
$ beet ls -a 'added:-6w..-4w'
189+
167190
Date *intervals*, like the numeric intervals described above, are separated by
168191
two dots (``..``). You can specify a start, an end, or both.
169192

test/test_datequery.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from __future__ import division, absolute_import, print_function
1919

2020
from test import _common
21-
from datetime import datetime
21+
from datetime import datetime, timedelta
2222
import unittest
2323
import time
2424
from beets.dbcore.query import _parse_periods, DateInterval, DateQuery,\
@@ -29,6 +29,10 @@ def _date(string):
2929
return datetime.strptime(string, '%Y-%m-%dT%H:%M:%S')
3030

3131

32+
def _datepattern(datetimedate):
33+
return datetimedate.strftime('%Y-%m-%dT%H:%M:%S')
34+
35+
3236
class DateIntervalTest(unittest.TestCase):
3337
def test_year_precision_intervals(self):
3438
self.assertContains('2000..2001', '2000-01-01T00:00:00')
@@ -44,6 +48,9 @@ def test_year_precision_intervals(self):
4448
self.assertContains('..2001', '2001-12-31T23:59:59')
4549
self.assertExcludes('..2001', '2002-01-01T00:00:00')
4650

51+
self.assertContains('-1d..1d', _datepattern(datetime.now()))
52+
self.assertExcludes('-2d..-1d', _datepattern(datetime.now()))
53+
4754
def test_day_precision_intervals(self):
4855
self.assertContains('2000-06-20..2000-06-20', '2000-06-20T00:00:00')
4956
self.assertContains('2000-06-20..2000-06-20', '2000-06-20T10:20:30')
@@ -161,6 +168,87 @@ def test_single_day_nonmatch_fast(self):
161168
self.assertEqual(len(matched), 0)
162169

163170

171+
class DateQueryTestRelative(_common.LibTestCase):
172+
def setUp(self):
173+
super(DateQueryTestRelative, self).setUp()
174+
self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M'))
175+
self.i.store()
176+
177+
def test_single_month_match_fast(self):
178+
query = DateQuery('added', datetime.now().strftime('%Y-%m'))
179+
matched = self.lib.items(query)
180+
self.assertEqual(len(matched), 1)
181+
182+
def test_single_month_nonmatch_fast(self):
183+
query = DateQuery('added', (datetime.now() + timedelta(days=30))
184+
.strftime('%Y-%m'))
185+
matched = self.lib.items(query)
186+
self.assertEqual(len(matched), 0)
187+
188+
def test_single_month_match_slow(self):
189+
query = DateQuery('added', datetime.now().strftime('%Y-%m'))
190+
self.assertTrue(query.match(self.i))
191+
192+
def test_single_month_nonmatch_slow(self):
193+
query = DateQuery('added', (datetime.now() + timedelta(days=30))
194+
.strftime('%Y-%m'))
195+
self.assertFalse(query.match(self.i))
196+
197+
def test_single_day_match_fast(self):
198+
query = DateQuery('added', datetime.now().strftime('%Y-%m-%d'))
199+
matched = self.lib.items(query)
200+
self.assertEqual(len(matched), 1)
201+
202+
def test_single_day_nonmatch_fast(self):
203+
query = DateQuery('added', (datetime.now() + timedelta(days=1))
204+
.strftime('%Y-%m-%d'))
205+
matched = self.lib.items(query)
206+
self.assertEqual(len(matched), 0)
207+
208+
209+
class DateQueryTestRelativeMore(_common.LibTestCase):
210+
def setUp(self):
211+
super(DateQueryTestRelativeMore, self).setUp()
212+
self.i.added = _parsetime(datetime.now().strftime('%Y-%m-%d %H:%M'))
213+
self.i.store()
214+
215+
def test_relative(self):
216+
for timespan in ['d', 'w', 'm', 'y']:
217+
query = DateQuery('added', '-4' + timespan + '..+4' + timespan)
218+
matched = self.lib.items(query)
219+
self.assertEqual(len(matched), 1)
220+
221+
def test_relative_fail(self):
222+
for timespan in ['d', 'w', 'm', 'y']:
223+
query = DateQuery('added', '-2' + timespan + '..-1' + timespan)
224+
matched = self.lib.items(query)
225+
self.assertEqual(len(matched), 0)
226+
227+
def test_start_relative(self):
228+
for timespan in ['d', 'w', 'm', 'y']:
229+
query = DateQuery('added', '-4' + timespan + '..')
230+
matched = self.lib.items(query)
231+
self.assertEqual(len(matched), 1)
232+
233+
def test_start_relative_fail(self):
234+
for timespan in ['d', 'w', 'm', 'y']:
235+
query = DateQuery('added', '4' + timespan + '..')
236+
matched = self.lib.items(query)
237+
self.assertEqual(len(matched), 0)
238+
239+
def test_end_relative(self):
240+
for timespan in ['d', 'w', 'm', 'y']:
241+
query = DateQuery('added', '..+4' + timespan)
242+
matched = self.lib.items(query)
243+
self.assertEqual(len(matched), 1)
244+
245+
def test_end_relative_fail(self):
246+
for timespan in ['d', 'w', 'm', 'y']:
247+
query = DateQuery('added', '..-4' + timespan)
248+
matched = self.lib.items(query)
249+
self.assertEqual(len(matched), 0)
250+
251+
164252
class DateQueryConstructTest(unittest.TestCase):
165253
def test_long_numbers(self):
166254
with self.assertRaises(InvalidQueryArgumentValueError):

0 commit comments

Comments
 (0)