Is there an easy way to convert ISO 8601 duration to timedelta?
Asked Answered
P

8

60

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

Pelota answered 2/5, 2016 at 6:25 Comment(4)
"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.Cohbath
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.Pelota
I don't understand why this question was closed as "too broad". It is very focussed.Petes
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?Giant
P
70

I found isodate library to do exactly what I want

isodate.parse_duration('PT1H5M26S')
  • You can read the source code for the function here
Pelota answered 2/5, 2016 at 6:44 Comment(1)
Note that isodate appears to be unmaintained (since 2021)Monostome
M
10

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'
Monostome answered 11/11, 2021 at 9:58 Comment(4)
pandas >= 1.2.0 is required to parse an ISO 8601 duration strings, see the pandas release notesPointblank
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...)Monostome
Note that contrarily to Alkindus answer, it returns ValueError: Invalid ISO 8601 Duration format on P3Y6M4DT12H30M5S (string source).Harty
That is because it isn't expressible as a timedelta, because the exact duration depends on the specific start or endpoint. In this case, pandas.tseries.frequencies might be useful (but note that OP specifically requested a timedelta)Monostome
C
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
Cygnus answered 6/10, 2020 at 19:44 Comment(1)
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.Cystoid
C
2

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
Cystoid answered 10/3, 2021 at 17:26 Comment(0)
B
2

If you are using Pydantic:

>>> from datetime import timedelta
>>> from pydantic import TypeAdapter
>>> timedelta_adapter = TypeAdapter(timedelta)
>>> td = timedelta_adapter.validate_python("PT1H5M26S")
>>> td
datetime.timedelta(seconds=3926)

For datetime.timedelta to ISO 8601 string 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.

Bearded answered 7/2, 2024 at 21:19 Comment(0)
T
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://mcmap.net/q/145493/-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.
Tolbutamide answered 16/3, 2021 at 7:42 Comment(0)
J
1

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://mcmap.net/q/145493/-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
Janijania answered 24/11, 2022 at 8:45 Comment(0)
C
1

You can simply use a regular expression to parse ISO 8601 time durations, without pulling in external dependencies.

The following works with common D/H/M/S designators. Supporting Y/M/W designators is not implemented.

import datetime
import re


def parse_iso8601_duration(duration: str) -> datetime.timedelta:    
    pattern = r"^P(?:(?P<days>\d+\.\d+|\d*?)D)?T?(?:(?P<hours>\d+\.\d+|\d*?)H)?(?:(?P<minutes>\d+\.\d+|\d*?)M)?(?:(?P<seconds>\d+\.\d+|\d*?)S)?$"
    match = re.match(pattern, duration)
    if not match:
        raise ValueError(f"Invalid ISO 8601 duration: {duration}")
    parts = {k: float(v) for k, v in match.groupdict("0").items()}
    return datetime.timedelta(**parts)
Cyanite answered 20/10, 2023 at 15:22 Comment(1)
Regex black magic.Strangulate

© 2022 - 2025 — McMap. All rights reserved.