Skip to content

Instantly share code, notes, and snippets.

@santiagobasulto
Last active March 7, 2024 14:57
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save santiagobasulto/698f0ff660968200f873a2f9d1c4113c to your computer and use it in GitHub Desktop.
Save santiagobasulto/698f0ff660968200f873a2f9d1c4113c to your computer and use it in GitHub Desktop.
A simple script to parse human readable time deltas into Python datetime.timedeltas
import re
TIMEDELTA_REGEX = (r'((?P<days>-?\d+)d)?'
r'((?P<hours>-?\d+)h)?'
r'((?P<minutes>-?\d+)m)?')
TIMEDELTA_PATTERN = re.compile(TIMEDELTA_REGEX, re.IGNORECASE)
def parse_delta(delta):
""" Parses a human readable timedelta (3d5h19m) into a datetime.timedelta.
Delta includes:
* Xd days
* Xh hours
* Xm minutes
Values can be negative following timedelta's rules. Eg: -5h-30m
"""
match = TIMEDELTA_PATTERN.match(delta)
if match:
parts = {k: int(v) for k, v in match.groupdict().items() if v}
return timedelta(**parts)
def test_day_deltas():
assert parse_delta('3d') == timedelta(days=3)
assert parse_delta('-3d') == timedelta(days=-3)
assert parse_delta('-37D') == timedelta(days=-37)
def test_hours_deltas():
assert parse_delta('18h') == timedelta(hours=18)
assert parse_delta('-5h') == timedelta(hours=-5)
assert parse_delta('11H') == timedelta(hours=11)
def test_minute_deltas():
assert parse_delta('129m') == timedelta(minutes=129)
assert parse_delta('-68m') == timedelta(minutes=-68)
assert parse_delta('12M') == timedelta(minutes=12)
def test_combined_deltas():
assert parse_delta('3d5h') == timedelta(days=3, hours=5)
assert parse_delta('-3d-5h') == timedelta(days=-3, hours=-5)
assert parse_delta('13d4h19m') == timedelta(days=13, hours=4, minutes=19)
assert parse_delta('13d19m') == timedelta(days=13, minutes=19)
assert parse_delta('-13d-19m') == timedelta(days=-13, minutes=-19)
assert parse_delta('-13d19m') == timedelta(days=-13, minutes=19)
assert parse_delta('13d-19m') == timedelta(days=13, minutes=-19)
assert parse_delta('4h19m') == timedelta(hours=4, minutes=19)
assert parse_delta('-4h-19m') == timedelta(hours=-4, minutes=-19)
assert parse_delta('-4h19m') == timedelta(hours=-4, minutes=19)
assert parse_delta('4h-19m') == timedelta(hours=4, minutes=-19)
@alexpovel
Copy link

Nice!

The tests show the desired functionality. For bogus input, mainly the empty string and other, non-matching material, the if match condition does not work. All three capture groups are optional, so at the end of the day, that regex matches everything we throw at it. match will never be None, but always be an re.Match object, which always evaluates to True. This is the exact same issue as here.

There, one answer suggests negative lookahead, anchored to the start of the string: ^(?!$). This works so the regex no longer matches the empty string. However, it still matches any other invalid string:

In [1]: re.search("^(?!$)((?P<days>-?\d+)d)?((?P<hours>-?\d+)h)?((?P<minutes>-?\d+)m)?", "HELLO WORLD")
Out[1]: <re.Match object; span=(0, 0), match=''>
In [2]: bool(_)
Out[2]: True

I don't really know how to circumvent this. It is probably easiest to check for if match.group(): without arguments, the group method returns the entire match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment