Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relative date queries (continuation of #2418) #2598

Merged
merged 29 commits into from
Jun 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
47b0b54
trying relative dates
euri10 Jan 28, 2017
5624e24
trying relative dates
euri10 Jan 28, 2017
03dfaf9
wrong dateutil package
euri10 Jan 28, 2017
bee758a
some flake8 spacing
euri10 Jan 28, 2017
9bc75b0
need dateutil in install ?
euri10 Jan 28, 2017
868746b
tests
euri10 Jan 28, 2017
8d054f3
tests flake8 issues
euri10 Jan 28, 2017
2b89b90
tests flake8 fixed I think finally
euri10 Jan 28, 2017
3e76c21
without dateutil
euri10 Jan 28, 2017
af679de
using a pattern may avoid copy-paste error when used 3 times after
euri10 Jan 29, 2017
d48d1f8
fixed E226 flake8
euri10 Jan 29, 2017
e4a7d37
implementing changes asked
euri10 Jan 29, 2017
c9177f2
removed unrelated PR changes
euri10 Jan 29, 2017
f0aca5e
Explain relative dates
euri10 Jan 30, 2017
d2cd4c0
Change relative date's format to further simplify it
euri10 Jan 31, 2017
6664b65
Merge branch 'relativedate'
euri10 Jun 15, 2017
92c118f
Corrected some flake8 errors
euri10 Jun 15, 2017
527d0a8
Corrected some flake8 errors
euri10 Jun 15, 2017
ae3f9bf
Corrected some flake8 errors, overindentation and pycharm reformat
euri10 Jun 15, 2017
6f97787
Merge branch 'master' into euri10-master
sampsyo Jun 15, 2017
26940b6
Merge branch 'master' into relativedate
sampsyo Jun 15, 2017
8a6c8cd
Simplify the docs a bit for relative dates
sampsyo Jun 15, 2017
130c581
Some formatting cleanup for relative dates
sampsyo Jun 15, 2017
01cf0d0
Merge remote-tracking branch 'upstream/master'
euri10 Jun 16, 2017
93f064f
Renamed relative to relative_units
euri10 Jun 16, 2017
eaae3fb
Merge branch 'relativedate'
euri10 Jun 16, 2017
690ed73
Move the relative date docs downward
sampsyo Jun 16, 2017
b1b4272
Refinements to the relative date docs
sampsyo Jun 16, 2017
a52d3d5
Changelog for relative dates (#2418, #2598)
sampsyo Jun 16, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions beets/dbcore/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -575,9 +578,20 @@ def __init__(self, date, precision):

@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):
Expand All @@ -593,10 +607,27 @@ def find_date_and_format(string):

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)

Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions docs/reference/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
90 changes: 89 additions & 1 deletion test/test_datequery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,\
Expand All @@ -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')
Expand All @@ -44,6 +48,9 @@ def test_year_precision_intervals(self):
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')
Expand Down Expand Up @@ -161,6 +168,87 @@ def test_single_day_nonmatch_fast(self):
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):
Expand Down