Convert a datetime.timedelta into ISO 8601 duration in Python?
Asked Answered
L

5

19

Given a datetime.timedelta, how to convert it into an ISO 8601 duration string?

For example, given datetime.timedelta(0, 18, 179651), how do I get 'PT18.179651S'?


For the reverse, see Is there an easy way to convert ISO 8601 duration to timedelta?.

Lassie answered 27/11, 2014 at 10:20 Comment(2)
The isodate module on PyPI can do that. You can look into its source code to see how it's implemented.Litchi
@Litchi It works quite well through isodate: isodate.duration_isoformat(timedelta). Thanks.Lassie
P
12

Although the datetime module contains an implementation for a ISO 8601 notation for datetime or date objects, it does not currently (Python 3.7) support the same for timedelta objects. However, the isodate module (pypi link) has functionality to generate a duration string in ISO 8601 notation:

In [15]: import isodate, datetime

In [16]: print(isodate.duration_isoformat(datetime.datetime.now() - datetime.datetime(1985, 8, 13, 15)))
P12148DT4H20M39.47017S

which means 12148 days, 4 hours, 20 minutes, 39.47017 seconds.

Perpendicular answered 16/11, 2018 at 19:21 Comment(2)
Do you have a link to the official timedelta string format. My google search comes up empty.Patellate
@Patellate As my answer states, it's ISO 8601 notation.Perpendicular
C
5

This is a function from Tin Can Python project (Apache License 2.0) that can do the conversion:

def iso8601(value):
    # split seconds to larger units
    seconds = value.total_seconds()
    minutes, seconds = divmod(seconds, 60)
    hours, minutes = divmod(minutes, 60)
    days, hours = divmod(hours, 24)
    days, hours, minutes = map(int, (days, hours, minutes))
    seconds = round(seconds, 6)

    ## build date
    date = ''
    if days:
        date = '%sD' % days

    ## build time
    time = u'T'
    # hours
    bigger_exists = date or hours
    if bigger_exists:
        time += '{:02}H'.format(hours)
    # minutes
    bigger_exists = bigger_exists or minutes
    if bigger_exists:
      time += '{:02}M'.format(minutes)
    # seconds
    if seconds.is_integer():
        seconds = '{:02}'.format(int(seconds))
    else:
        # 9 chars long w/leading 0, 6 digits after decimal
        seconds = '%09.6f' % seconds
    # remove trailing zeros
    seconds = seconds.rstrip('0')
    time += '{}S'.format(seconds)
    return u'P' + date + time

E.g.

>>> iso8601(datetime.timedelta(0, 18, 179651))
'PT18.179651S'
Cathrinecathryn answered 27/11, 2014 at 10:57 Comment(1)
Better to use: isodate.duration_isoformat(timedelta)Brigidbrigida
P
1

I tried to write a shorter function:

def iso8601(tdelta):
    ts = tdelta.total_seconds()
    d = int(ts // 86400)
    s = round(ts % 60, 6)
    hms = int(ts // 3600 % 24), int(ts // 60 % 60), s if s % 1 != 0 else int(s)
    t = ''.join([str(p[0]) + p[1] for p in zip(hms, ['H', 'M', 'S']) if p[0]])
    sep = 'T' if any(hms) else ''
    return 'P' + (str(d) + 'D' if d else '') + sep + (t if ts else 'T0S')

E.g.

>>> iso8601(datetime.timedelta(0, 18, 179651))
'PT18.179651S'

This may be less readable for some, but I think it's easier to take it apart and inline it in certain use cases. For example, if you're not using timedelta but already have the duration in integer seconds ts and, say, your durations are always shorter than a day, this becomes:

hms = ts // 3600, ts // 60 % 60, ts % 60
t = ''.join([str(p[0]) + p[1] for p in zip(hms, ['H', 'M', 'S']) if p[0]])
ts_iso8601 = 'PT' + t if ts else 'PT0S'
Pharisaism answered 7/5, 2020 at 17:53 Comment(3)
I personally find your code very hard to read. longer variable names would be a big improvementSufism
I totally agree, and even mention that in the answer. The function (first block) is not something I'd use in production verbatim, it's more of an exercise in brevity that leads to the subset three-line solution (third block), which could be tolerable (or easily clarified) in certain cases.Pharisaism
yea, but don't forget that a lot of people just blindly copy & paste SO answers and many beginners will adapt this coding style. That's why I personally try to stick to production quality code on SO as much as possible.Sufism
P
1

If you are using Pydantic:

>>> from datetime import timedelta
>>> from pydantic import TypeAdapter
>>> timedelta_adapter = TypeAdapter(timedelta)
>>> td = timedelta(0, 18, 179651)
>>> timedelta_adapter.dump_python(td, mode="json")
'PT18.179651S'

For ISO 8601 string to datetime.timedelta see here.

Explanation

Pydantic is a data validation library. It accepts and outputs ISO 8601 strings if declared type is datetime.timedelta. By using a TypeAdapter, you can do validation/dump operations on a single type rather than on a Pydantic model.

Pestilence answered 7/2 at 21:31 Comment(0)
P
0

Here is a self-contained implementation from Pydantic (source):

def timedelta_isoformat(td: datetime.timedelta) -> str:
    """ISO 8601 encoding for Python timedelta object."""
    minutes, seconds = divmod(td.seconds, 60)
    hours, minutes = divmod(minutes, 60)
    return f'{"-" if td.days < 0 else ""}P{abs(td.days)}DT{hours:d}H{minutes:d}M{seconds:d}.{td.microseconds:06d}S'
Pestilence answered 7/2 at 22:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.