Skip to content

Commit 45665de

Browse files
committed
reimplement datetime parsing logic
Signed-off-by: Kunz, Immanuel <immanuel.kunz@aisec.fraunhofer.de>
1 parent 6bd9052 commit 45665de

3 files changed

Lines changed: 64 additions & 6 deletions

File tree

src/univers/datetime.py

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
# Visit https://aboutcode.org and https://github.com/aboutcode-org/univers for support and download.
55

66
import re
7-
from datetime import datetime
8-
from datetime import timezone
9-
7+
from datetime import datetime, timedelta, timezone
108

119
class DatetimeVersion:
1210
"""
@@ -18,18 +16,73 @@ class DatetimeVersion:
1816
VERSION_PATTERN = re.compile(
1917
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$"
2018
)
19+
_TIME_TZ_RE = re.compile(
20+
r"^(?P<h>\d{2}):(?P<M>\d{2}):(?P<s>\d{2})(?:\.(?P<f>\d+))?(?P<tz>Z|[+-]\d{2}:\d{2})$"
21+
)
2122

2223
def __init__(self, version):
2324
version = str(version).strip()
2425
if not self.is_valid(version):
2526
raise InvalidVersionError(version)
2627

27-
# fromisoformat doesn't accept the "Z" suffix prior to 3.11, so we normalize it:
28+
# save the original
29+
self.original = version
30+
31+
# normalize Z to +00:00 to make tz parsing uniform
2832
if version.endswith("Z"):
2933
version = version[:-1] + "+00:00"
3034

31-
self.original = version
32-
self.parsed_stamp = datetime.fromisoformat(version).astimezone(timezone.utc)
35+
# split into date and time+tz parts
36+
date_part, time_tz_part = version.split("T", 1)
37+
38+
# parse the date-only portion first using fromisoformat
39+
# (datetime.fromisoformat accepts date-only strings)
40+
try:
41+
dt = datetime.fromisoformat(date_part)
42+
except ValueError:
43+
raise InvalidVersionError(version)
44+
45+
# parse time and timezone with regex
46+
m = self._TIME_TZ_RE.fullmatch(time_tz_part)
47+
if not m:
48+
raise InvalidVersionError(version)
49+
50+
hour = int(m.group("h"))
51+
minute = int(m.group("M"))
52+
second = int(m.group("s"))
53+
frac = m.group("f") or ""
54+
# ensure microseconds length is exactly 6 (truncate or pad), because datetime requires that
55+
if frac:
56+
micro = int((frac[:6]).ljust(6, "0"))
57+
else:
58+
micro = 0
59+
60+
tz_text = m.group("tz")
61+
if tz_text == "Z":
62+
tzinfo = timezone.utc
63+
else:
64+
# tz_text is in form +HH:MM or -HH:MM
65+
sign = 1 if tz_text[0] == "+" else -1
66+
tzh = int(tz_text[1:3])
67+
tzm = int(tz_text[4:6])
68+
offset = sign * (tzh * 3600 + tzm * 60)
69+
tzinfo = timezone(timedelta(seconds=offset))
70+
71+
# construct aware datetime for the exact instant
72+
dt = datetime(
73+
year=dt.year,
74+
month=dt.month,
75+
day=dt.day,
76+
hour=hour,
77+
minute=minute,
78+
second=second,
79+
microsecond=micro,
80+
tzinfo=tzinfo,
81+
)
82+
83+
# canonicalize to UTC for comparisons/hashing
84+
self.parsed_stamp = dt.astimezone(timezone.utc)
85+
3386

3487
def __eq__(self, other):
3588
return self.parsed_stamp == other.parsed_stamp

tests/test_version_range.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,9 @@ def test_version_range_intdot():
371371

372372

373373
def test_version_range_datetime():
374+
assert DatetimeVersion("2021-05-05T01:02:03.1234+00:00") == DatetimeVersion(
375+
"2021-05-05T01:02:03.1234+00:00"
376+
)
374377
assert DatetimeVersion("2021-05-05T01:02:03.1234Z") == DatetimeVersion(
375378
"2021-05-05T01:02:03.1234Z"
376379
)

tests/test_versions.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,5 @@ def test_datetime_version():
240240
assert DatetimeVersion("2022-10-28T18:30:00Z") <= DatetimeVersion("2023-10-28T18:30:00Z")
241241
assert DatetimeVersion("2024-10-28T18:30:00Z") > DatetimeVersion("2023-10-28T18:30:00Z")
242242
assert DatetimeVersion("2023-10-28T19:30:00+01:00") == DatetimeVersion("2023-10-28T18:30:00Z")
243+
assert not DatetimeVersion.is_valid("2023-10-28Z19:30:00+01:00")
244+
assert not DatetimeVersion.is_valid("10-10-2023T19:30:00+01:00")

0 commit comments

Comments
 (0)