From 493f7e9e1ed7d68df102de6c5926a55521b145c9 Mon Sep 17 00:00:00 2001 From: Matthew Schinckel Date: Thu, 13 Apr 2017 11:33:35 +0930 Subject: [PATCH] Fixed #28076 -- Added support for PostgreSQL's interval format to parse_duration(). --- django/utils/dateparse.py | 22 +++++++++++++++++++--- docs/ref/utils.txt | 7 ++++++- tests/utils_tests/test_dateparse.py | 15 +++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/django/utils/dateparse.py b/django/utils/dateparse.py index 48d15d4c1e..ffb37afcdf 100644 --- a/django/utils/dateparse.py +++ b/django/utils/dateparse.py @@ -50,6 +50,20 @@ iso8601_duration_re = re.compile( r'$' ) +# Support PostgreSQL's day-time interval format, e.g. "3 days 04:05:06". The +# year-month and mixed intervals cannot be converted to a timedelta and thus +# aren't accepted. +postgres_interval_re = re.compile( + r'^' + r'(?:(?P-?\d+) (days? ?))?' + r'(?:(?P[-+])?' + r'(?P\d+):' + r'(?P\d\d):' + r'(?P\d\d)' + r'(?:\.(?P\d{1,6}))?' + r')?$' +) + def parse_date(value): """Parse a string and return a datetime.date. @@ -114,17 +128,19 @@ def parse_duration(value): The preferred format for durations in Django is '%d %H:%M:%S.%f'. - Also supports ISO 8601 representation. + Also supports ISO 8601 representation and PostgreSQL's day-time interval + format. """ match = standard_duration_re.match(value) if not match: - match = iso8601_duration_re.match(value) + match = iso8601_duration_re.match(value) or postgres_interval_re.match(value) if match: kw = match.groupdict() + days = datetime.timedelta(float(kw.pop('days', 0) or 0)) sign = -1 if kw.pop('sign', '+') == '-' else 1 if kw.get('microseconds'): kw['microseconds'] = kw['microseconds'].ljust(6, '0') if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'): kw['microseconds'] = '-' + kw['microseconds'] kw = {k: float(v) for k, v in kw.items() if v is not None} - return sign * datetime.timedelta(**kw) + return days + sign * datetime.timedelta(**kw) diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 985514473f..93a4715793 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -143,7 +143,12 @@ The functions defined in this module share the following properties: Parses a string and returns a :class:`datetime.timedelta`. Expects data in the format ``"DD HH:MM:SS.uuuuuu"`` or as specified by ISO - 8601 (e.g. ``P4DT1H15M20S`` which is equivalent to ``4 1:15:20``). + 8601 (e.g. ``P4DT1H15M20S`` which is equivalent to ``4 1:15:20``) or + PostgreSQL's day-time interval format (e.g. ``3 days 04:05:06``). + + .. versionchanged:: 2.0 + + Support for PostgreSQL's interval format was added. ``django.utils.decorators`` =========================== diff --git a/tests/utils_tests/test_dateparse.py b/tests/utils_tests/test_dateparse.py index 7127e812ae..8d464278ce 100644 --- a/tests/utils_tests/test_dateparse.py +++ b/tests/utils_tests/test_dateparse.py @@ -65,6 +65,21 @@ class DurationParseTests(unittest.TestCase): with self.subTest(delta=delta): self.assertEqual(parse_duration(format(delta)), delta) + def test_parse_postgresql_format(self): + test_values = ( + ('1 day', timedelta(1)), + ('1 day 0:00:01', timedelta(days=1, seconds=1)), + ('1 day -0:00:01', timedelta(days=1, seconds=-1)), + ('-1 day -0:00:01', timedelta(days=-1, seconds=-1)), + ('-1 day +0:00:01', timedelta(days=-1, seconds=1)), + ('4 days 0:15:30.1', timedelta(days=4, minutes=15, seconds=30, milliseconds=100)), + ('4 days 0:15:30.0001', timedelta(days=4, minutes=15, seconds=30, microseconds=100)), + ('-4 days -15:00:30', timedelta(days=-4, hours=-15, seconds=-30)), + ) + for source, expected in test_values: + with self.subTest(source=source): + self.assertEqual(parse_duration(source), expected) + def test_seconds(self): self.assertEqual(parse_duration('30'), timedelta(seconds=30))