Commit 1a742825 authored by nmartensen's avatar nmartensen
Browse files

implement reading of ISO 8601 durations

This implements reading the limited subset of the ISO 8601 duration
specification that seems to be used by Excel when writing in OOXML
strict mode:

 - Only hours, minutes, and seconds are understood
 - Only the point is accepted as decimal separator
 - The duration string always starts with 'PT'

--HG--
branch : 3.0
parent 27a2a16bf748
Pipeline #18807 passed with stage
in 2 minutes and 53 seconds
......@@ -25,6 +25,7 @@ ISO_REGEX = re.compile(r'''
(?P<date>(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}))?T?
(?P<time>(?P<hour>\d{2}):(?P<minute>\d{2})(:(?P<second>\d{2})(?P<microsecond>\.\d{1,3})?)?)?Z?''',
re.VERBOSE)
ISO_DURATION = re.compile(r'PT((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+(\.\d{1,3})?)S)?')
def to_ISO8601(dt):
......@@ -41,30 +42,42 @@ def from_ISO8601(formatted_string):
Times B.1.2 and B.2.2
Datetimes B.1.3 and B.2.3
There is no concept of timedeltas
There is no concept of timedeltas in the specification, but Excel
writes them (in strict OOXML mode), so these are also understood.
"""
if not formatted_string:
return None
match = ISO_REGEX.match(formatted_string)
if not match:
raise ValueError("Invalid datetime value {}".format(formatted_string))
if match and any(match.groups()):
parts = match.groupdict(0)
for key in ["year", "month", "day", "hour", "minute", "second"]:
if parts[key]:
parts[key] = int(parts[key])
if parts["microsecond"]:
parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
if not parts["date"]:
dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
elif not parts["time"]:
dt = datetime.date(parts['year'], parts['month'], parts['day'])
else:
del parts["time"]
del parts["date"]
dt = datetime.datetime(**parts)
return dt
match = ISO_DURATION.match(formatted_string)
if match and any(match.groups()):
parts = match.groupdict(0)
for key, val in parts.items():
if val:
parts[key]=float(val)
return datetime.timedelta(**parts)
raise ValueError("Invalid datetime value {}".format(formatted_string))
parts = match.groupdict(0)
for key in ["year", "month", "day", "hour", "minute", "second"]:
if parts[key]:
parts[key] = int(parts[key])
if parts["microsecond"]:
parts["microsecond"] = int(float(parts['microsecond']) * 1_000_000)
if not parts["date"]:
dt = datetime.time(parts['hour'], parts['minute'], parts['second'], parts["microsecond"])
elif not parts["time"]:
dt = datetime.date(parts['year'], parts['month'], parts['day'])
else:
del parts["time"]
del parts["date"]
dt = datetime.datetime(**parts)
return dt
def to_excel(dt, epoch=WINDOWS_EPOCH):
......
......@@ -47,6 +47,10 @@ def test_iso_regex(value, group, expected):
("2020-12-03T12:19:01.003Z", datetime(2020, 12, 3, 12, 19, 1, 3000)),
("2020-12-03T12:19:01.3Z", datetime(2020, 12, 3, 12, 19, 1, 300_000)),
("2020-12-03T12:19:01.03", datetime(2020, 12, 3, 12, 19, 1, 30_000)),
("PT0M", timedelta(0)),
("PT2H0M1S", timedelta(hours=2, seconds=1)),
("PT25H20M1.1S", timedelta(days=1, hours=1, minutes=20, seconds=1.1)),
("PT25H70M1.123S", timedelta(days=1, hours=2, minutes=10, seconds=1.123)),
]
)
def test_from_iso(value, expected):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment