mirror of
https://github.com/beetbox/beets.git
synced 2025-12-26 18:43:38 +01:00
Merge branch 'relativedate'
Solved conflicts with upstream of new parse classmethod of DateQuery # Conflicts: # beets/dbcore/query.py
This commit is contained in:
commit
6664b656f4
3 changed files with 148 additions and 4 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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