diff --git a/src/undate/dateformat/base.py b/src/undate/dateformat/base.py index f4435f4..7349f7f 100644 --- a/src/undate/dateformat/base.py +++ b/src/undate/dateformat/base.py @@ -15,8 +15,8 @@ import importlib import logging import pkgutil -from typing import Dict, Type -from functools import lru_cache # functools.cache not available until 3.9 +from typing import Dict, Type, Union +from functools import lru_cache logger = logging.getLogger(__name__) diff --git a/src/undate/dateformat/edtf/formatter.py b/src/undate/dateformat/edtf/formatter.py index dee193f..e9a88f6 100644 --- a/src/undate/dateformat/edtf/formatter.py +++ b/src/undate/dateformat/edtf/formatter.py @@ -35,7 +35,17 @@ def _convert_missing_digits( return value.replace(old_missing_digit, EDTF_UNSPECIFIED_DIGIT) return None - def to_string(self, undate: Undate) -> str: + def to_string(self, undate: Union[Undate, UndateInterval]) -> str: + if isinstance(undate, Undate): + return self._undate_to_string(undate) + elif isinstance(undate, UndateInterval): + # NOTE: what is the difference between an open interval and unknown start/end? + # spec distinguishes between these, open is ".." but unknown is "" + start = self._undate_to_string(undate.earliest) if undate.earliest else ".." + end = self._undate_to_string(undate.latest) if undate.latest else ".." + return f"{start}/{end}" + + def _undate_to_string(self, undate: Undate) -> str: # in theory it's possible to use the parser and reconstruct using a tree, # but that seems much more complicated and would be harder to read parts = [] diff --git a/src/undate/dateformat/iso8601.py b/src/undate/dateformat/iso8601.py index 0b3a3b5..ff8cb7e 100644 --- a/src/undate/dateformat/iso8601.py +++ b/src/undate/dateformat/iso8601.py @@ -49,12 +49,25 @@ def _parse_single_date(self, value: str) -> Undate: # Argument of type "int | None" cannot be assigned to parameter "formatter" of type "BaseDateFormat | None" in function "__init__" return Undate(*date_parts) # type: ignore - def to_string(self, undate: Undate) -> str: + def to_string(self, undate: Union[Undate, UndateInterval]) -> str: + if isinstance(undate, Undate): + return self._undate_to_string(undate) + elif isinstance(undate, UndateInterval): + # strictly speaking I don't think ISO8601 supports open-ended ranges + # should we add an exception for dates that can't be represented by a particular format? + # (we'll likely need it for uncertain/approx, which ISO8601 doesn't handle') + start = self._undate_to_string(undate.earliest) if undate.earliest else "" + end = self._undate_to_string(undate.latest) if undate.latest else "" + return f"{start}/{end}" + + def _undate_to_string(self, undate: Undate) -> str: # serialize to iso format for simplicity, for now date_parts: List[Union[str, None]] = [] # for each part of the date that is known, generate the string format # then combine # TODO: should error if we have year and day but no month + # TODO: may want to refactor and take advantage of the year/month/day properties + # added for use in EDTF formatter code for date_portion, iso_format in self.iso_format.items(): if undate.is_known(date_portion): # NOTE: datetime strftime for %Y for 3-digit year