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

11.7k views Asked by At

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?.

5

There are 5 answers

2
gerrit On BEST ANSWER

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.

1
manlio On

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'
3
naktinis On

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'
0
DurandA On

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.

0
DurandA On

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'