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 23 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
39 changes: 36 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 = {'y': 365, 'm': 30, 'w': 7, 'd': 1}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A better name might be relative_units.

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[timespan]
date = datetime.now() + \
timedelta(days=int(quantity) * days) * multiplier
string = date.strftime(cls.date_formats[5][0])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@euri10: Turning things back into a string seems a little odd. Can we just return cls(date, something) here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, at first I didn't know if the ordinal was used anywhere, so turning things back into a string was a way to use the find_date_and_format function that already exists


# 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 All @@ -606,6 +637,8 @@ def open_right_endpoint(self):
"""
precision = self.precision
date = self.date
if 'relative' == self.precision:
return date
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can self.precision ever be "relative" in this version? Is this possibly out of date?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, no use

if 'year' == self.precision:
return date.replace(year=date.year + 1, month=1)
elif 'month' == precision:
Expand Down
23 changes: 23 additions & 0 deletions docs/reference/query.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,29 @@ Dates are written separated by hyphens, like ``year-month-day``, but the month
and day are optional. If you leave out the day, for example, you will get
matches for the whole month.

You can also use relative dates to the current time.
A relative date begins with an ``@``.
It looks like ``@-3w``, ``@2m`` or ``@-4d`` which means the date 3 weeks ago,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we need to remove the @ here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, corrected

the date 2 months from now and the date 4 days ago.
A relative date consists of three parts:
- ``+`` or ``-`` sign is optional and defaults to ``+``. The ``+`` sign will
add a time quantity to the current date while the ``-`` sign will do the
opposite
- a number follows and indicates the amount to add or substract
- a final letter ends and represents the amount in either days, weeks, months or
years (``d``, ``w``, ``m`` or ``y``)

Please note that this relative calculation makes the assumption of 30 days per
month and 365 days per year.

Here is an example that finds all the albums added between now and last week::

$ beet ls -a 'added:-1w..'

Find all items added in a 2 weeks period 4 weeks ago::

$ beet ls -a 'added:-6w..-4w'

Date *intervals*, like the numeric intervals described above, are separated by
two dots (``..``). You can specify a start, an end, or both.

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