diff --git a/sdk/basyx/aas/model/datatypes.py b/sdk/basyx/aas/model/datatypes.py index e33b3e0b..351981c0 100644 --- a/sdk/basyx/aas/model/datatypes.py +++ b/sdk/basyx/aas/model/datatypes.py @@ -106,7 +106,12 @@ def __init__(self, year: int, month: int, tzinfo: Optional[datetime.tzinfo] = No self.tzinfo: Optional[datetime.tzinfo] = tzinfo def into_date(self, day: int = 1) -> Date: - return Date(self.year, self.month, day, self.tzinfo) + try: + return Date(self.year, self.month, day, self.tzinfo) + except ValueError as e: + if self.year < 0: + raise ValueError("Negative years are not supported by Python's `datetime` library.") from e + raise e @classmethod def from_date(cls, date: datetime.date) -> "GYearMonth": @@ -131,7 +136,12 @@ def __init__(self, year: int, tzinfo: Optional[datetime.tzinfo] = None): self.tzinfo: Optional[datetime.tzinfo] = tzinfo def into_date(self, month: int = 1, day: int = 1) -> Date: - return Date(self.year, month, day, self.tzinfo) + try: + return Date(self.year, month, day, self.tzinfo) + except ValueError as e: + if self.year < 0: + raise ValueError("Negative years are not supported by Python's `datetime` library.") from e + raise e @classmethod def from_date(cls, date: datetime.date) -> "GYear": @@ -166,7 +176,7 @@ def into_date(self, year: int = 1970) -> Date: @classmethod def from_date(cls, date: datetime.date) -> "GMonthDay": tzinfo = date.tzinfo if hasattr(date, 'tzinfo') else None # type: ignore - return cls(date.month, date.year, tzinfo) + return cls(date.month, date.day, tzinfo) def __eq__(self, other: object) -> bool: if not isinstance(other, GMonthDay): @@ -638,10 +648,10 @@ def _parse_xsd_bool(value: str) -> Boolean: raise ValueError("Invalid literal for XSD bool type") -GYEAR_RE = re.compile(r'^(\d\d\d\d)([+\-]\d\d:\d\d|Z)?$') +GYEAR_RE = re.compile(r'^(-?)(\d{4,})([+\-]\d\d:\d\d|Z)?$') GMONTH_RE = re.compile(r'^--(\d\d)([+\-]\d\d:\d\d|Z)?$') GDAY_RE = re.compile(r'^---(\d\d)([+\-]\d\d:\d\d|Z)?$') -GYEARMONTH_RE = re.compile(r'^(\d\d\d\d)-(\d\d)([+\-]\d\d:\d\d|Z)?$') +GYEARMONTH_RE = re.compile(r'^(-?)(\d{4,})-(\d\d)([+\-]\d\d:\d\d|Z)?$') GMONTHDAY_RE = re.compile(r'^--(\d\d)-(\d\d)([+\-]\d\d:\d\d|Z)?$') @@ -649,7 +659,10 @@ def _parse_xsd_gyear(value: str) -> GYear: match = GYEAR_RE.match(value) if not match: raise ValueError("Value is not a valid XSD GYear string") - return GYear(int(match[1]), _parse_xsd_date_tzinfo(match[2])) + year = int(match[2]) + if match[1]: + year = -year + return GYear(year, _parse_xsd_date_tzinfo(match[3])) def _parse_xsd_gmonth(value: str) -> GMonth: @@ -670,7 +683,10 @@ def _parse_xsd_gyearmonth(value: str) -> GYearMonth: match = GYEARMONTH_RE.match(value) if not match: raise ValueError("Value is not a valid XSD GYearMonth string") - return GYearMonth(int(match[1]), int(match[2]), _parse_xsd_date_tzinfo(match[3])) + year = int(match[2]) + if match[1]: + year = -year + return GYearMonth(year, int(match[3]), _parse_xsd_date_tzinfo(match[4])) def _parse_xsd_gmonthday(value: str) -> GMonthDay: diff --git a/sdk/test/model/test_datatypes.py b/sdk/test/model/test_datatypes.py index f314d302..c8126400 100644 --- a/sdk/test/model/test_datatypes.py +++ b/sdk/test/model/test_datatypes.py @@ -156,10 +156,18 @@ def test_serialize_date(self) -> None: def test_parse_partial_dates(self) -> None: self.assertEqual(model.datatypes.GYear(2019), model.datatypes.from_xsd("2019", model.datatypes.GYear)) + self.assertEqual(model.datatypes.GYear(-2001), + model.datatypes.from_xsd("-2001", model.datatypes.GYear)) + self.assertEqual(model.datatypes.GYear(20000), + model.datatypes.from_xsd("20000", model.datatypes.GYear)) self.assertEqual(model.datatypes.GMonth(7), model.datatypes.from_xsd("--07", model.datatypes.GMonth)) self.assertEqual(model.datatypes.GYearMonth(2020, 5), model.datatypes.from_xsd("2020-05", model.datatypes.GYearMonth)) + self.assertEqual(model.datatypes.GYearMonth(-2001, 10), + model.datatypes.from_xsd("-2001-10", model.datatypes.GYearMonth)) + self.assertEqual(model.datatypes.GYearMonth(20000, 5), + model.datatypes.from_xsd("20000-05", model.datatypes.GYearMonth)) self.assertEqual(model.datatypes.GMonthDay(12, 6), model.datatypes.from_xsd("--12-06", model.datatypes.GMonthDay)) self.assertEqual(model.datatypes.GDay(23), @@ -180,6 +188,14 @@ def test_parse_partial_dates(self) -> None: model.datatypes.from_xsd("10-10", model.datatypes.GYearMonth) self.assertEqual("Value is not a valid XSD GYearMonth string", str(cm.exception)) + def test_partial_dates_negative_year_into_date(self) -> None: + # Python's `datetime` library does not support negative years. Converting a G-Class with a negative year into a + # `Date` must therefore fail with a clear error message instead of the cryptic "year -2001 is out of range". + for value in (model.datatypes.GYear(-2001), model.datatypes.GYearMonth(-2001, 5)): + with self.assertRaises(ValueError) as cm: + value.into_date() + self.assertEqual("Negative years are not supported by Python's `datetime` library.", str(cm.exception)) + def test_copy_date(self) -> None: date = model.datatypes.Date(2020, 1, 24) date_copy_shallow = copy.copy(date)