Merge branch 'relativedate'

Solved conflicts with upstream of new parse classmethod of DateQuery

# Conflicts:
#	beets/dbcore/query.py
This commit is contained in:
euri10 2017-06-15 08:59:43 +02:00
commit 6664b656f4
3 changed files with 148 additions and 4 deletions

View file

@ -533,6 +533,7 @@ class Period(object):
instants of time during January 2014.
"""
precisions = ('year', 'month', 'day', 'hour', 'minute', 'second')
date_formats = (
('%Y',), # year
@ -542,6 +543,8 @@ 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}
def __init__(self, date, precision):
"""Create a period with the given date (a `datetime` object) and
@ -555,9 +558,21 @@ class Period(object):
@classmethod
def parse(cls, string):
"""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.
"""Parse a date and return a `Period` object or `None` if the
string is empty.
Depending on the string, the date can be absolute or
relative.
An absolute date has to be like one of the date_formats '%Y' or '%Y-%m'
or '%Y-%m-%d'
A relative date consists of three parts:
- a ``+`` 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.
"""
def find_date_and_format(string):
@ -573,6 +588,20 @@ class Period(object):
if not string:
return None
pattern_dq = '(?P<sign>[+|-]?)(?P<quantity>[0-9]+)(?P<timespan>[y|m|w|d])' # noqa: E501
match_dq = re.match(pattern_dq, string)
# test if the string matches the relative date pattern, add the parsed
# quantity to now in that case
if match_dq is not None:
sign = match_dq.group('sign')
quantity = match_dq.group('quantity')
timespan = match_dq.group('timespan')
multiplier = -1 if sign == '-' else 1
days = cls.relative[timespan]
date = datetime.now() + multiplier * timedelta(days=int(quantity) * days)
string = date.strftime(cls.date_formats[5][0])
date, ordinal = find_date_and_format(string)
if date is None:
raise InvalidQueryArgumentValueError(string,
@ -586,6 +615,8 @@ class Period(object):
"""
precision = self.precision
date = self.date
if 'relative' == self.precision:
return date
if 'year' == self.precision:
return date.replace(year=date.year + 1, month=1)
elif 'month' == precision:
@ -647,6 +678,7 @@ class DateQuery(FieldQuery):
The value of a date field can be matched against a date interval by
using an ellipsis interval syntax similar to that of NumericQuery.
"""
def __init__(self, field, pattern, fast=True):
super(DateQuery, self).__init__(field, pattern, fast)
start, end = _parse_periods(pattern)
@ -690,6 +722,7 @@ class DurationQuery(NumericQuery):
Raises InvalidQueryError when the pattern does not represent an int, float
or M:SS time interval.
"""
def _convert(self, s):
"""Convert a M:SS or numeric string to a float.

View file

@ -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,
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.

View file

@ -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):