Quellcode durchsuchen

Fixed #28076 -- Added support for PostgreSQL's interval format to parse_duration().

Matthew Schinckel vor 8 Jahren
Ursprung
Commit
493f7e9e1e
3 geänderte Dateien mit 40 neuen und 4 gelöschten Zeilen
  1. 19 3
      django/utils/dateparse.py
  2. 6 1
      docs/ref/utils.txt
  3. 15 0
      tests/utils_tests/test_dateparse.py

+ 19 - 3
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<days>-?\d+) (days? ?))?'
+    r'(?:(?P<sign>[-+])?'
+    r'(?P<hours>\d+):'
+    r'(?P<minutes>\d\d):'
+    r'(?P<seconds>\d\d)'
+    r'(?:\.(?P<microseconds>\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)

+ 6 - 1
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``
 ===========================

+ 15 - 0
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))