From c64d4b21e72bd5bb112e0440519f965a57cf0f4a Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Tue, 30 Jun 2026 15:35:13 +0200
Subject: [PATCH 1/2] Fix: Handle XSD end-of-day midnight notation 24:00:00
Before, parsing XSD timestamps with an hour of 24 causes a ValueError.
Now, the parser in `sdk/basyx/aas/model/datatypes.py` correctly identifies 24:00:00 as midnight, handles the day rollover for `DateTime`, and normalizes `Time` to 00:00:00.
Unit tests for valid and invalid edge cases are added to `sdk/test/model/test_datatypes.py` inside `TestDateTimeTypes`.
Fixes #564
---
sdk/basyx/aas/model/datatypes.py | 19 ++++++++++++++++---
sdk/test/model/test_datatypes.py | 20 ++++++++++++++++++++
2 files changed, 36 insertions(+), 3 deletions(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index d5acc6d45..8ae5af0d5 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -615,16 +615,29 @@ def _parse_xsd_datetime(value: str) -> DateTime:
if match[1]:
raise ValueError("Negative Dates are not supported by Python")
microseconds = int(float(match[8]) * 1e6) if match[8] else 0
- return DateTime(int(match[2]), int(match[3]), int(match[4]), int(match[5]), int(match[6]), int(match[7]),
+ hour = int(match[5])
+ is_midnight_24 = False
+ if hour == 24:
+ if int(match[6]) != 0 or int(match[7]) != 0 or microseconds != 0:
+ raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ hour = 0
+ is_midnight_24 = True
+
+ res = DateTime(int(match[2]), int(match[3]), int(match[4]), hour, int(match[6]), int(match[7]),
microseconds, _parse_xsd_date_tzinfo(match[9]))
-
+ return res + datetime.timedelta(days=1) if is_midnight_24 else res
def _parse_xsd_time(value: str) -> Time:
match = TIME_RE.match(value)
if not match:
raise ValueError("Value is not a valid XSD datetime string")
microseconds = int(float(match[4]) * 1e6) if match[4] else 0
- return Time(int(match[1]), int(match[2]), int(match[3]), microseconds, _parse_xsd_date_tzinfo(match[5]))
+ hour = int(match[1])
+ if hour == 24:
+ if int(match[2]) != 0 or int(match[3]) != 0 or microseconds != 0:
+ raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
+ hour = 0
+ return Time(hour, int(match[2]), int(match[3]), microseconds, _parse_xsd_date_tzinfo(match[5]))
def _parse_xsd_bool(value: str) -> Boolean:
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index b83c5e5fb..d06a47ba7 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -243,7 +243,27 @@ def test_serialize_time(self) -> None:
datetime.time(15, 25, 17, tzinfo=datetime.timezone.utc)))
self.assertEqual("15:25:17.250000+01:00", model.datatypes.xsd_repr(
datetime.time(15, 25, 17, 250000, tzinfo=datetime.timezone(datetime.timedelta(hours=1)))))
+
+ def test_parse_datetime_midnight_24(self) -> None:
+ res = model.datatypes.from_xsd("2020-01-24T24:00:00", model.datatypes.DateTime)
+ self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0), res)
+ res_tz = model.datatypes.from_xsd("2020-01-24T24:00:00Z", model.datatypes.DateTime)
+ self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0, tzinfo=datetime.timezone.utc), res_tz)
+
+ def test_parse_datetime_midnight_24_invalid(self) -> None:
+ with self.assertRaises(ValueError) as cm:
+ model.datatypes.from_xsd("2020-01-24T24:01:00", model.datatypes.DateTime)
+ self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+
+ def test_parse_time_midnight_24(self) -> None:
+ res = model.datatypes.from_xsd("24:00:00", model.datatypes.Time)
+ self.assertEqual(datetime.time(0, 0, 0), res)
+ def test_parse_time_midnight_24_invalid(self) -> None:
+ with self.assertRaises(ValueError) as cm:
+ model.datatypes.from_xsd("24:00:01", model.datatypes.Time)
+ self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+
def test_trivial_cast(self) -> None:
val = model.datatypes.trivial_cast(datetime.date(2017, 11, 13), model.datatypes.Date)
self.assertEqual(model.datatypes.Date(2017, 11, 13), val)
From 1ceb325f53149fd0e424aec13ef9c54689747d0f Mon Sep 17 00:00:00 2001
From: Paul Gerber
Date: Tue, 30 Jun 2026 15:58:41 +0200
Subject: [PATCH 2/2] Fixed Too Long Lines Issues
---
sdk/basyx/aas/model/datatypes.py | 5 +++--
sdk/test/model/test_datatypes.py | 14 ++++++++++----
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py
index 8ae5af0d5..2c696ff30 100644
--- a/sdk/basyx/aas/model/datatypes.py
+++ b/sdk/basyx/aas/model/datatypes.py
@@ -622,11 +622,12 @@ def _parse_xsd_datetime(value: str) -> DateTime:
raise ValueError("Invalid time: 24:00:00.000000 is the only valid representation of midnight")
hour = 0
is_midnight_24 = True
-
+
res = DateTime(int(match[2]), int(match[3]), int(match[4]), hour, int(match[6]), int(match[7]),
- microseconds, _parse_xsd_date_tzinfo(match[9]))
+ microseconds, _parse_xsd_date_tzinfo(match[9]))
return res + datetime.timedelta(days=1) if is_midnight_24 else res
+
def _parse_xsd_time(value: str) -> Time:
match = TIME_RE.match(value)
if not match:
diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py
index d06a47ba7..0f7b71e27 100644
--- a/sdk/test/model/test_datatypes.py
+++ b/sdk/test/model/test_datatypes.py
@@ -243,7 +243,7 @@ def test_serialize_time(self) -> None:
datetime.time(15, 25, 17, tzinfo=datetime.timezone.utc)))
self.assertEqual("15:25:17.250000+01:00", model.datatypes.xsd_repr(
datetime.time(15, 25, 17, 250000, tzinfo=datetime.timezone(datetime.timedelta(hours=1)))))
-
+
def test_parse_datetime_midnight_24(self) -> None:
res = model.datatypes.from_xsd("2020-01-24T24:00:00", model.datatypes.DateTime)
self.assertEqual(datetime.datetime(2020, 1, 25, 0, 0, 0), res)
@@ -253,7 +253,10 @@ def test_parse_datetime_midnight_24(self) -> None:
def test_parse_datetime_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("2020-01-24T24:01:00", model.datatypes.DateTime)
- self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
+ self.assertEqual(
+ "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
+ str(cm.exception)
+ )
def test_parse_time_midnight_24(self) -> None:
res = model.datatypes.from_xsd("24:00:00", model.datatypes.Time)
@@ -262,8 +265,11 @@ def test_parse_time_midnight_24(self) -> None:
def test_parse_time_midnight_24_invalid(self) -> None:
with self.assertRaises(ValueError) as cm:
model.datatypes.from_xsd("24:00:01", model.datatypes.Time)
- self.assertEqual("Invalid time: 24:00:00.000000 is the only valid representation of midnight", str(cm.exception))
-
+ self.assertEqual(
+ "Invalid time: 24:00:00.000000 is the only valid representation of midnight",
+ str(cm.exception)
+ )
+
def test_trivial_cast(self) -> None:
val = model.datatypes.trivial_cast(datetime.date(2017, 11, 13), model.datatypes.Date)
self.assertEqual(model.datatypes.Date(2017, 11, 13), val)