Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper method for building relativedelta from ISO8601 duration #1210

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS.md
Expand Up @@ -99,6 +99,7 @@ switch, and thus all their contributions are dual-licensed.
- Raymond Cha (gh: @weatherpattern) **D**
- Ridhi Mahajan <ridhikmahajan@MASKED> **D**
- Robin Henriksson Törnström <gh: @MrRawbin> **D**
- Robert Morris <gh: @morrissimo> **D**
- Roy Williams <rwilliams@MASKED>
- Rustem Saiargaliev (gh: @amureki) **D**
- Satyabrat Bhol <satyabrat35@MASKED> (gh: @Satyabrat35) **D**
Expand Down
4 changes: 4 additions & 0 deletions changelog.d/1210.feature.rst
@@ -0,0 +1,4 @@
Adds helper method ``dateutil.relativedelta.from_iso8610()``
to convert an ISO8601 duration into a ``relativedelta`` instance
NOTE: does not support ISO8601 week durations (eg, "P[n]W")
Added by @morrissimo (refs gh issues #854)
32 changes: 31 additions & 1 deletion src/dateutil/relativedelta.py
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
import datetime
import calendar

import operator
from math import copysign
import re

from six import integer_types
from warnings import warn
Expand Down Expand Up @@ -593,6 +593,36 @@ def __repr__(self):
attrs=", ".join(l))


# https://en.wikipedia.org/wiki/ISO_8601#Durations
ISO8601_PATT = r"""
P # all ISO8601 interval strings start with P (required)
((?P<years>[-\d]{1,5})Y)? # match 1-4 digits for year, with 1 optional place for a negative sign
((?P<months>[-\d]{1,3})M)? # match 1-2 digits for month, with 1 optional place for a negative sign
((?P<days>[-\d]{1,3})D)? # match 1-2 digits for day, with 1 optional place for a negative sign
(
T # time field separator
((?P<hours>[-\d]{1,3})H)? # match 1-2 digits for hour, with 1 optional place for a negative sign
((?P<minutes>[-\d]{1,3})M)? # match 1-2 digits for minute, with 1 optional place for a negative sign
((?P<seconds>[-\d]{1,3})S)? # match 1-2 digits for seconds, with 1 optional place for a negative sign
)?
"""
ISO8601_RE = re.compile(ISO8601_PATT, re.VERBOSE)


def from_iso8601(raw):
"""
Parse an ISO8601 formatted duration into a relativedelta instance

https://en.wikipedia.org/wiki/ISO_8601#Durations
https://dateutil.readthedocs.io/en/stable/relativedelta.html
"""
rd_kwargs = dict(years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0)
matches = re.search(ISO8601_RE, raw)
if matches:
rd_kwargs.update({field: int(value or 0) for field, value in matches.groupdict().items()})
return relativedelta(**rd_kwargs)


def _sign(x):
return int(copysign(1, x))

Expand Down
202 changes: 201 additions & 1 deletion tests/test_relativedelta.py
Expand Up @@ -8,7 +8,7 @@

import pytest

from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU
from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU, from_iso8601


class RelativeDeltaTest(unittest.TestCase):
Expand Down Expand Up @@ -764,4 +764,204 @@ def test_minus_height_days_set_minus_one_week(self):
self.assertEqual(rd.weeks, -1)


class RelativeDeltaFromISO88601DurationTest(unittest.TestCase):

def test_invalid(self):
raw = "invalid"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_valid_but_empty(self):
for raw in ["P", "PT"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_year_only(self):
for raw in ["P1Y", "P1Y0M0DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 1)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
for raw in ["P-1Y", "P-1Y0M0DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, -1)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_month_only(self):
for raw in ["P1M", "P0Y1M0DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 1)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
for raw in ["P-1M", "P0Y-1M0DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, -1)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_day_only(self):
for raw in ["P1D", "P0Y0M1DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 1)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
for raw in ["P-1D", "P0Y0M-1DT0H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, -1)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_hour_only(self):
for raw in ["PT1H", "P0Y0M0DT1H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 1)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
for raw in ["PT-1H", "P0Y0M0DT-1H0M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, -1)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_minute_only(self):
for raw in ["PT1M", "P0Y0M0DT0H1M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 1)
self.assertEqual(rd.seconds, 0)
for raw in ["PT-1M", "P0Y0M0DT0H-1M0S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, -1)
self.assertEqual(rd.seconds, 0)

def test_second_only(self):
for raw in ["PT1S", "P0Y0M0DT0H0M1S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 1)
for raw in ["PT-1S", "P0Y0M0DT0H0M-1S"]:
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, -1)

def test_date_fragment(self):
raw = "P1Y2M3D"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 1)
self.assertEqual(rd.months, 2)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 3)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
# with the T(ime) separator but no time values
raw = "P1Y2M3DT"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 1)
self.assertEqual(rd.months, 2)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 3)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)

def test_time_fragment(self):
raw = "PT1H2M3S"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, 1)
self.assertEqual(rd.minutes, 2)
self.assertEqual(rd.seconds, 3)

def test_realistic(self):
# 1 month, 15 days
raw = "P1M15D"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 1)
self.assertEqual(rd.weeks, 2)
self.assertEqual(rd.days, 15)
self.assertEqual(rd.hours, 0)
self.assertEqual(rd.minutes, 0)
self.assertEqual(rd.seconds, 0)
# -14 hours, -30 minutes
raw = "PT-14H-30M"
rd = from_iso8601(raw)
self.assertEqual(rd.years, 0)
self.assertEqual(rd.months, 0)
self.assertEqual(rd.weeks, 0)
self.assertEqual(rd.days, 0)
self.assertEqual(rd.hours, -14)
self.assertEqual(rd.minutes, -30)
self.assertEqual(rd.seconds, 0)



# vim:ts=4:sw=4:et