50

How can I convert an ISO 8601 duration string to datetime.timedelta?

I tried just instantiating timedelta with the duration string and a format string, but I get an exception:

>>> from datetime import timedelta
>>> timedelta("PT1H5M26S", "T%H%M%S")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for timedelta seconds component: str

For the reverse, see Convert a datetime.timedelta into ISO 8601 duration in Python?.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
Alkindus
  • 2,064
  • 2
  • 16
  • 16
  • 1
    *"a string of datetime.timedelta format"* doesn't make sense, I assume you mean a `datetime.timedelta` *object*. The standard lib doesn't have parsing for deltas, but there are packages on PyPI for you to `pip install` that do. If you want to know how to do it yourself, I think that's too broad for SO; you should have a go and see where (if anywhere!) you get too stuck to continue. – jonrsharpe May 02 '16 at 06:30
  • yes you are right. I want to convert it timedelta object. I can code my parser but, I am searching for if there is a simple solution. – Alkindus May 02 '16 at 06:36
  • 14
    I don't understand why this question was closed as "too broad". It is very focussed. – gerrit Nov 16 '18 at 16:13
  • What is a `datetime.timedelta` ? Is it part of a package? can you give a concrete example of what you want vs what you got? – SherylHohman Dec 18 '19 at 02:18

6 Answers6

65

I found isodate library to do exactly what I want

isodate.parse_duration('PT1H5M26S')
  • You can read the source code for the function here
Louis Maddox
  • 5,226
  • 5
  • 36
  • 66
Alkindus
  • 2,064
  • 2
  • 16
  • 16
8

If you're using Pandas, you could use pandas.Timedelta. The constructor accepts an ISO 8601 string, and pandas.Timedelta.isoformat you can format the instance back to a string:

>>> import pandas as pd
>>> dt = pd.Timedelta("PT1H5M26S")
>>> dt
Timedelta('0 days 01:05:26')
>>> dt.isoformat()
'P0DT1H5M26S'
Joren
  • 3,068
  • 25
  • 44
  • pandas >= 1.2.0 is required to parse an ISO 8601 duration strings, see the [pandas release notes](https://pandas.pydata.org/pandas-docs/stable/whatsnew/v1.2.0.html#timedelta) – hertzsprung Dec 19 '22 at 16:27
  • Pandas 1.2 has been released >2 years ago; so I hope that everyone has upgraded since then. (although I'm painfully aware that python 2.7 is being used by mostly data scientists...) – Joren Jan 02 '23 at 23:20
4

Here's a solution without a new package, but only works if you're dealing with a max duration expressed in days. That limitation makes sense though, because as others have pointed out (1):

Given that the timedelta has more than "a month's" worth of days, how would you describe it using the ISO8601 duration notation without referencing a specific point in time? Conversely, given your example, "P3Y6M4DT12H30M5S", how would you convert that into a timedelta without knowing which exact years and months this duration refers to? Timedelta objects are very precise beasts, which is almost certainly why they don't support "years" and "months" args in their constructors.

import datetime


def get_isosplit(s, split):
    if split in s:
        n, s = s.split(split)
    else:
        n = 0
    return n, s


def parse_isoduration(s):
        
    # Remove prefix
    s = s.split('P')[-1]
    
    # Step through letter dividers
    days, s = get_isosplit(s, 'D')
    _, s = get_isosplit(s, 'T')
    hours, s = get_isosplit(s, 'H')
    minutes, s = get_isosplit(s, 'M')
    seconds, s = get_isosplit(s, 'S')

    # Convert all to seconds
    dt = datetime.timedelta(days=int(days), hours=int(hours), minutes=int(minutes), seconds=int(seconds))
    return int(dt.total_seconds())
> parse_isoduration("PT1H5M26S")
3926
rer
  • 1,198
  • 2
  • 13
  • 24
  • Note that the values are not necessarily integer (e.g. MPD manifests for DASH media streams for MP4 on the web), this presumes they all are and coerces to integer. – Louis Maddox Mar 10 '21 at 17:07
1

Great question, obviously the "right" solution depends on your expectations for the input (a more reliable data source doesn't need as much input validation).

My approach to parse an ISO8601 duration timestamp only checks that the "PT" prefix is present and will not assume integer values for any of the units:

from datetime import timedelta

def parse_isoduration(isostring, as_dict=False):
    """
    Parse the ISO8601 duration string as hours, minutes, seconds
    """
    separators = {
        "PT": None,
        "W": "weeks",
        "D": "days",
        "H": "hours",
        "M": "minutes",
        "S": "seconds",
    }
    duration_vals = {}
    for sep, unit in separators.items():
        partitioned = isostring.partition(sep)
        if partitioned[1] == sep:
            # Matched this unit
            isostring = partitioned[2]
            if sep == "PT":
                continue # Successful prefix match
            dur_str = partitioned[0]
            dur_val = float(dur_str) if "." in dur_str else int(dur_str)
            duration_vals.update({unit: dur_val})
        else:
            if sep == "PT":
                raise ValueError("Missing PT prefix")
            else:
                # No match for this unit: it's absent
                duration_vals.update({unit: 0})
    if as_dict:
        return duration_vals
    else:
        return tuple(duration_vals.values())

dur_isostr = "PT3H2M59.989333S"
dur_tuple = parse_isoduration(dur_isostr)
dur_dict = parse_isoduration(dur_isostr, as_dict=True)
td = timedelta(**dur_dict)
s = td.total_seconds()

>>> dur_tuple
(0, 0, 3, 2, 59.989333)
>>> dur_dict
{'weeks': 0, 'days': 0, 'hours': 3, 'minutes': 2, 'seconds': 59.989333}
>>> td
datetime.timedelta(seconds=10979, microseconds=989333)
>>> s
10979.989333
Louis Maddox
  • 5,226
  • 5
  • 36
  • 66
1

Based on @r3robertson a more complete, yet not perfect, version

def parse_isoduration(s):
""" Parse a str ISO-8601 Duration: https://en.wikipedia.org/wiki/ISO_8601#Durations
Originally copied from:
https://stackoverflow.com/questions/36976138/is-there-an-easy-way-to-convert-iso-8601-duration-to-timedelta
:param s:
:return:
"""

# ToDo [40]: Can't handle legal ISO3106 ""PT1M""

def get_isosplit(s, split):
    if split in s:
        n, s = s.split(split, 1)
    else:
        n = '0'
    return n.replace(',', '.'), s  # to handle like "P0,5Y"

s = s.split('P', 1)[-1]  # Remove prefix
s_yr, s = get_isosplit(s, 'Y')  # Step through letter dividers
s_mo, s = get_isosplit(s, 'M')
s_dy, s = get_isosplit(s, 'D')
_, s = get_isosplit(s, 'T')
s_hr, s = get_isosplit(s, 'H')
s_mi, s = get_isosplit(s, 'M')
s_sc, s = get_isosplit(s, 'S')
n_yr = float(s_yr) * 365  # These are approximations that I can live with
n_mo = float(s_mo) * 30.4  # But they are not correct!
dt = datetime.timedelta(days=n_yr+n_mo+float(s_dy), hours=float(s_hr), minutes=float(s_mi), seconds=float(s_sc))
return dt  # int(dt.total_seconds())  # original code wanted to return as seconds, we don't.
Martin
  • 391
  • 1
  • 4
  • 15
0

This is my modification(Martin, rer answers) to support weeks attribute and return milliseconds. Some durations may use PT15.460S fractions.

def parse_isoduration(str):
## https://stackoverflow.com/questions/36976138/is-there-an-easy-way-to-convert-iso-8601-duration-to-timedelta
## Parse the ISO8601 duration as years,months,weeks,days, hours,minutes,seconds
## Returns: milliseconds
## Examples: "PT1H30M15.460S", "P5DT4M", "P2WT3H"
    def get_isosplit(str, split):
        if split in str:
            n, str = str.split(split, 1)
        else:
            n = '0'
        return n.replace(',', '.'), str  # to handle like "P0,5Y"

    str = str.split('P', 1)[-1]  # Remove prefix
    s_yr, str = get_isosplit(str, 'Y')  # Step through letter dividers
    s_mo, str = get_isosplit(str, 'M')
    s_wk, str = get_isosplit(str, 'W')
    s_dy, str = get_isosplit(str, 'D')
    _, str    = get_isosplit(str, 'T')
    s_hr, str = get_isosplit(str, 'H')
    s_mi, str = get_isosplit(str, 'M')
    s_sc, str = get_isosplit(str, 'S')
    n_yr = float(s_yr) * 365   # approx days for year, month, week
    n_mo = float(s_mo) * 30.4
    n_wk = float(s_wk) * 7
    dt = datetime.timedelta(days=n_yr+n_mo+n_wk+float(s_dy), hours=float(s_hr), minutes=float(s_mi), seconds=float(s_sc))
    return int(dt.total_seconds()*1000) ## int(dt.total_seconds()) | dt
Whome
  • 10,181
  • 6
  • 53
  • 65