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)