Incorrect handling of microseconds in utils.datetime.from_ISO8601
In utils/datetime.py
, the function from_ISO8601
handles fractional seconds incorrectly. For example:
>>> import openpyxl
>>> dt_string = "2020-12-13T22:52:03.341"
>>> openpyxl.utils.datetime.from_ISO8601(dt_string)
datetime.datetime(2020, 12, 13, 22, 52, 3, 34)
The 34
in the datetime
object is the number of microseconds, which is not the same as the (rounded) 0.34 of a second specified in the string. The answer should instead be datetime.datetime(2020, 12, 13, 22, 52, 3, 340_000)
Edited test case to reproduce:
# openpyxl/utils/tests/test_datetime.py
@pytest.mark.parametrize("value, expected",
[
("2011-06-30T13:35:26Z", datetime(2011, 6, 30, 13, 35, 26)),
("2013-03-04T12:19:01.00Z", datetime(2013, 3, 4, 12, 19, 1)),
("2011-06-30", date(2011, 6, 30)),
("12:19:01", time(12, 19, 1)),
("2020-12-13T22:52:03.341", datetime(2020, 12, 13, 22, 52, 3, 340_000)),
]
)
def test_from_iso(value, expected):
from ..datetime import from_ISO8601
assert from_ISO8601(value) == expected
Suggested function:
# openpyxl/utils/datetime.py
def from_ISO8601(formatted_string):
"""Convert from a timestamp string to a date, time, or datetime object.
According to 18.17.4 in the specification the following ISO 8601 formats
are supported.
Dates B.1.1 and B.2.1
Times B.1.2 and B.2.2
Datetimes B.1.3 and B.2.3
There is no concept of timedeltas
"""
match = ISO_REGEX.match(formatted_string)
if not match:
raise ValueError("Invalid datetime value {}".format(formatted_string))
parts = {
k: int(v) for k, v in match.groupdict().items() if v is not None and v.isdigit()
}
if "hour" not in parts:
return datetime.date(
year=parts["year"], month=parts["month"], day=parts["day"]
)
microseconds = int(str(parts["ms"]).ljust(6, "0")) if "ms" in parts else 0
if "year" not in parts:
return datetime.time(
hour=parts["hour"],
minute=parts["minute"],
second=parts["second"],
microsecond=microseconds
)
return datetime.datetime(
year=parts["year"],
month=parts["month"],
day=parts["day"],
hour=parts["hour"],
minute=parts["minute"],
second=parts["second"],
microsecond=microseconds
)