@@ -422,7 +422,9 @@ def is_known(self, part: str) -> bool:
422422 return isinstance (self .initial_values [part ], int )
423423
424424 def is_partially_known (self , part : str ) -> bool :
425+ # TODO: should XX / XXXX really be considered partially known? other code seems to assume this, so we'll preserve the behavior
425426 return isinstance (self .initial_values [part ], str )
427+ # and self.initial_values[part].replace(self.MISSING_DIGIT, "") != ""
426428
427429 @property
428430 def year (self ) -> Optional [str ]:
@@ -464,6 +466,47 @@ def _get_date_part(self, part: str) -> Optional[str]:
464466 value = self .initial_values .get (part )
465467 return str (value ) if value else None
466468
469+ @property
470+ def possible_years (self ) -> list [int ] | range :
471+ """A list or range of possible years for this date in the original calendar.
472+ Returns a list with a single year for dates with fully-known years."""
473+ if self .known_year :
474+ return [self .earliest .year ]
475+
476+ step = 1
477+ if (
478+ self .is_partially_known ("year" )
479+ and str (self .year ).replace (self .MISSING_DIGIT , "" ) != ""
480+ ):
481+ # determine the smallest step size for the missing digit
482+ earliest_year = int (str (self .year ).replace (self .MISSING_DIGIT , "0" ))
483+ latest_year = int (str (self .year ).replace (self .MISSING_DIGIT , "9" ))
484+ missing_digit_place = len (str (self .year )) - str (self .year ).rfind (
485+ self .MISSING_DIGIT
486+ )
487+ # convert place to 1, 10, 100, 1000, etc.
488+ step = 10 ** (missing_digit_place - 1 )
489+ return range (earliest_year , latest_year + 1 , step )
490+ else : # year is fully unknown
491+ # returning range from min year to max year is not useful in any scenario!
492+ raise ValueError (
493+ "Possible years cannot be returned for completely unknown year"
494+ )
495+
496+ return [] # shouldn't get here, but mypy complains
497+
498+ @property
499+ def representative_years (self ) -> list [int ]:
500+ """A list of representative years for this date."""
501+ try :
502+ # todo: filter by calendar to minimum needed
503+ return list (self .possible_years )
504+ except ValueError :
505+ return [
506+ self .calendar_converter .LEAP_YEAR ,
507+ self .calendar_converter .NON_LEAP_YEAR ,
508+ ]
509+
467510 def duration (self ) -> Timedelta | UnDelta :
468511 """What is the duration of this date?
469512 Calculate based on earliest and latest date within range,
@@ -478,24 +521,6 @@ def duration(self) -> Timedelta | UnDelta:
478521 if self .precision == DatePrecision .DAY :
479522 return ONE_DAY
480523
481- # if year is unknown or partially unknown, month and year duration both need to calculate for
482- # variant years (leap year, non-leap year), since length may vary
483- possible_years = [self .earliest .year ]
484- if self .is_partially_known ("year" ):
485- # if year is partially known (e.g. 191X), get all possible years in range
486- # TODO: refactor into a function/property; combine/extract from missing digit min/max method
487- possible_years = [
488- int (str (self .year ).replace (self .MISSING_DIGIT , str (digit )))
489- for digit in range (0 , 10 )
490- ]
491- # TODO: once this is working, make more efficient by only getting representative years from the calendar
492- elif not self .known_year : # completely unknown year
493- # TODO: should leap-year specific logic shift to the calendars,
494- # since it works differently depending on the calendar?
495- possible_years = [
496- self .calendar_converter .LEAP_YEAR ,
497- self .calendar_converter .NON_LEAP_YEAR ,
498- ]
499524 possible_max_days = set ()
500525
501526 # if precision is month and year is unknown,
@@ -507,7 +532,7 @@ def duration(self) -> Timedelta | UnDelta:
507532 # should always be day-precision dates
508533 if self .earliest .month is not None and self .latest .month is not None :
509534 for possible_month in range (self .earliest .month , self .latest .month + 1 ):
510- for year in possible_years :
535+ for year in self . representative_years :
511536 possible_max_days .add (
512537 self .calendar_converter .max_day (year , possible_month )
513538 )
@@ -517,7 +542,8 @@ def duration(self) -> Timedelta | UnDelta:
517542 # this is currently hebrew-specific due to the way the start/end of year wraps for that calendar
518543 # with contextlib.suppress(NotImplementedError):
519544 possible_max_days = {
520- self .calendar_converter .days_in_year (y ) for y in possible_years
545+ self .calendar_converter .days_in_year (y )
546+ for y in self .representative_years
521547 }
522548
523549 # if there is more than one possible value for number of days
0 commit comments