Skip to content

Commit

Permalink
Fixed #26656 -- Added duration (timedelta) support to DjangoJSONEncoder.
Browse files Browse the repository at this point in the history
  • Loading branch information
willhardy authored and timgraham committed Jul 14, 2016
1 parent a7b5dfd commit 8ef78b8
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 5 deletions.
3 changes: 3 additions & 0 deletions django/core/serializers/json.py
Expand Up @@ -16,6 +16,7 @@
Deserializer as PythonDeserializer, Serializer as PythonSerializer,
)
from django.utils import six
from django.utils.duration import duration_iso_string
from django.utils.functional import Promise
from django.utils.timezone import is_aware

Expand Down Expand Up @@ -108,6 +109,8 @@ def default(self, o):
if o.microsecond:
r = r[:12]
return r
elif isinstance(o, datetime.timedelta):
return duration_iso_string(o)
elif isinstance(o, decimal.Decimal):
return str(o)
elif isinstance(o, uuid.UUID):
Expand Down
6 changes: 4 additions & 2 deletions django/utils/dateparse.py
Expand Up @@ -40,7 +40,8 @@
# Support the sections of ISO 8601 date representation that are accepted by
# timedelta
iso8601_duration_re = re.compile(
r'^P'
r'^(?P<sign>[-+]?)'
r'P'
r'(?:(?P<days>\d+(.\d+)?)D)?'
r'(?:T'
r'(?:(?P<hours>\d+(.\d+)?)H)?'
Expand Down Expand Up @@ -121,7 +122,8 @@ def parse_duration(value):
match = iso8601_duration_re.match(value)
if match:
kw = match.groupdict()
sign = -1 if kw.pop('sign', '+') == '-' else 1
if kw.get('microseconds'):
kw['microseconds'] = kw['microseconds'].ljust(6, '0')
kw = {k: float(v) for k, v in six.iteritems(kw) if v is not None}
return datetime.timedelta(**kw)
return sign * datetime.timedelta(**kw)
23 changes: 21 additions & 2 deletions django/utils/duration.py
@@ -1,7 +1,7 @@
"""Version of str(timedelta) which is not English specific."""
import datetime


def duration_string(duration):
def _get_duration_components(duration):
days = duration.days
seconds = duration.seconds
microseconds = duration.microseconds
Expand All @@ -12,10 +12,29 @@ def duration_string(duration):
hours = minutes // 60
minutes = minutes % 60

return days, hours, minutes, seconds, microseconds


def duration_string(duration):
"""Version of str(timedelta) which is not English specific."""
days, hours, minutes, seconds, microseconds = _get_duration_components(duration)

string = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
if days:
string = '{} '.format(days) + string
if microseconds:
string += '.{:06d}'.format(microseconds)

return string


def duration_iso_string(duration):
if duration < datetime.timedelta(0):
sign = '-'
duration *= -1
else:
sign = ''

days, hours, minutes, seconds, microseconds = _get_duration_components(duration)
ms = '.{:06d}'.format(microseconds) if microseconds else ""
return '{}P{}DT{:02d}H{:02d}M{:02d}{}S'.format(sign, days, hours, minutes, seconds, ms)
4 changes: 4 additions & 0 deletions docs/releases/1.11.txt
Expand Up @@ -223,6 +223,10 @@ Serialization
can now be customized by passing a ``cls`` keyword argument to the
``serializers.serialize()`` function.

* :class:`~django.core.serializers.json.DjangoJSONEncoder` now serializes
:class:`~datetime.timedelta` objects (used by
:class:`~django.db.models.DurationField`).

Signals
~~~~~~~

Expand Down
9 changes: 9 additions & 0 deletions docs/topics/serialization.txt
Expand Up @@ -297,13 +297,22 @@ The JSON serializer uses ``DjangoJSONEncoder`` for encoding. A subclass of
:class:`~datetime.time`
A string of the form ``HH:MM:ss.sss`` as defined in `ECMA-262`_.

:class:`~datetime.timedelta`
A string representing a duration as defined in ISO-8601. For example,
``timedelta(days=1, hours=2, seconds=3.4)`` is represented as
``'P1DT02H00M03.400000S'``.

:class:`~decimal.Decimal`, ``Promise`` (``django.utils.functional.lazy()`` objects), :class:`~uuid.UUID`
A string representation of the object.

.. versionchanged:: 1.10

Support for ``Promise`` was added.

.. versionchanged:: 1.11

Support for :class:`~datetime.timedelta` was added.

.. _ecma-262: http://www.ecma-international.org/ecma-262/5.1/#sec-15.9.1.15

YAML
Expand Down
13 changes: 13 additions & 0 deletions tests/serializers/test_json.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import datetime
import decimal
import json
import re
Expand Down Expand Up @@ -302,3 +303,15 @@ def test_lazy_string_encoding(self):
json.dumps({'lang': ugettext_lazy("French")}, cls=DjangoJSONEncoder),
'{"lang": "Fran\\u00e7ais"}'
)

def test_timedelta(self):
duration = datetime.timedelta(days=1, hours=2, seconds=3)
self.assertEqual(
json.dumps({'duration': duration}, cls=DjangoJSONEncoder),
'{"duration": "P1DT02H00M03S"}'
)
duration = datetime.timedelta(0)
self.assertEqual(
json.dumps({'duration': duration}, cls=DjangoJSONEncoder),
'{"duration": "P0DT00H00M00S"}'
)
40 changes: 39 additions & 1 deletion tests/utils_tests/test_duration.py
Expand Up @@ -2,7 +2,7 @@
import unittest

from django.utils.dateparse import parse_duration
from django.utils.duration import duration_string
from django.utils.duration import duration_iso_string, duration_string


class TestDurationString(unittest.TestCase):
Expand Down Expand Up @@ -41,3 +41,41 @@ def test_microseconds(self):
def test_negative(self):
duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
self.assertEqual(parse_duration(duration_string(duration)), duration)


class TestISODurationString(unittest.TestCase):

def test_simple(self):
duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
self.assertEqual(duration_iso_string(duration), 'P0DT01H03M05S')

def test_days(self):
duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
self.assertEqual(duration_iso_string(duration), 'P1DT01H03M05S')

def test_microseconds(self):
duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
self.assertEqual(duration_iso_string(duration), 'P0DT01H03M05.012345S')

def test_negative(self):
duration = -1 * datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
self.assertEqual(duration_iso_string(duration), '-P1DT01H03M05S')


class TestParseISODurationRoundtrip(unittest.TestCase):

def test_simple(self):
duration = datetime.timedelta(hours=1, minutes=3, seconds=5)
self.assertEqual(parse_duration(duration_iso_string(duration)), duration)

def test_days(self):
duration = datetime.timedelta(days=1, hours=1, minutes=3, seconds=5)
self.assertEqual(parse_duration(duration_iso_string(duration)), duration)

def test_microseconds(self):
duration = datetime.timedelta(hours=1, minutes=3, seconds=5, microseconds=12345)
self.assertEqual(parse_duration(duration_iso_string(duration)), duration)

def test_negative(self):
duration = datetime.timedelta(days=-1, hours=1, minutes=3, seconds=5)
self.assertEqual(parse_duration(duration_iso_string(duration)).total_seconds(), duration.total_seconds())

0 comments on commit 8ef78b8

Please sign in to comment.