Skip to content

Commit 493f7e9

Browse files
schinckeltimgraham
authored andcommitted
Fixed #28076 -- Added support for PostgreSQL's interval format to parse_duration().
1 parent 684c0a3 commit 493f7e9

File tree

3 files changed

+40
-4
lines changed

3 files changed

+40
-4
lines changed

django/utils/dateparse.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@
5050
r'$'
5151
)
5252

53+
# Support PostgreSQL's day-time interval format, e.g. "3 days 04:05:06". The
54+
# year-month and mixed intervals cannot be converted to a timedelta and thus
55+
# aren't accepted.
56+
postgres_interval_re = re.compile(
57+
r'^'
58+
r'(?:(?P<days>-?\d+) (days? ?))?'
59+
r'(?:(?P<sign>[-+])?'
60+
r'(?P<hours>\d+):'
61+
r'(?P<minutes>\d\d):'
62+
r'(?P<seconds>\d\d)'
63+
r'(?:\.(?P<microseconds>\d{1,6}))?'
64+
r')?$'
65+
)
66+
5367

5468
def parse_date(value):
5569
"""Parse a string and return a datetime.date.
@@ -114,17 +128,19 @@ def parse_duration(value):
114128
115129
The preferred format for durations in Django is '%d %H:%M:%S.%f'.
116130
117-
Also supports ISO 8601 representation.
131+
Also supports ISO 8601 representation and PostgreSQL's day-time interval
132+
format.
118133
"""
119134
match = standard_duration_re.match(value)
120135
if not match:
121-
match = iso8601_duration_re.match(value)
136+
match = iso8601_duration_re.match(value) or postgres_interval_re.match(value)
122137
if match:
123138
kw = match.groupdict()
139+
days = datetime.timedelta(float(kw.pop('days', 0) or 0))
124140
sign = -1 if kw.pop('sign', '+') == '-' else 1
125141
if kw.get('microseconds'):
126142
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
127143
if kw.get('seconds') and kw.get('microseconds') and kw['seconds'].startswith('-'):
128144
kw['microseconds'] = '-' + kw['microseconds']
129145
kw = {k: float(v) for k, v in kw.items() if v is not None}
130-
return sign * datetime.timedelta(**kw)
146+
return days + sign * datetime.timedelta(**kw)

docs/ref/utils.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,12 @@ The functions defined in this module share the following properties:
143143
Parses a string and returns a :class:`datetime.timedelta`.
144144

145145
Expects data in the format ``"DD HH:MM:SS.uuuuuu"`` or as specified by ISO
146-
8601 (e.g. ``P4DT1H15M20S`` which is equivalent to ``4 1:15:20``).
146+
8601 (e.g. ``P4DT1H15M20S`` which is equivalent to ``4 1:15:20``) or
147+
PostgreSQL's day-time interval format (e.g. ``3 days 04:05:06``).
148+
149+
.. versionchanged:: 2.0
150+
151+
Support for PostgreSQL's interval format was added.
147152

148153
``django.utils.decorators``
149154
===========================

tests/utils_tests/test_dateparse.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,21 @@ def test_parse_python_format(self):
6565
with self.subTest(delta=delta):
6666
self.assertEqual(parse_duration(format(delta)), delta)
6767

68+
def test_parse_postgresql_format(self):
69+
test_values = (
70+
('1 day', timedelta(1)),
71+
('1 day 0:00:01', timedelta(days=1, seconds=1)),
72+
('1 day -0:00:01', timedelta(days=1, seconds=-1)),
73+
('-1 day -0:00:01', timedelta(days=-1, seconds=-1)),
74+
('-1 day +0:00:01', timedelta(days=-1, seconds=1)),
75+
('4 days 0:15:30.1', timedelta(days=4, minutes=15, seconds=30, milliseconds=100)),
76+
('4 days 0:15:30.0001', timedelta(days=4, minutes=15, seconds=30, microseconds=100)),
77+
('-4 days -15:00:30', timedelta(days=-4, hours=-15, seconds=-30)),
78+
)
79+
for source, expected in test_values:
80+
with self.subTest(source=source):
81+
self.assertEqual(parse_duration(source), expected)
82+
6883
def test_seconds(self):
6984
self.assertEqual(parse_duration('30'), timedelta(seconds=30))
7085

0 commit comments

Comments
 (0)