@@ -126,6 +126,7 @@ def calculate_earliest_latest(self, year, month, day):
126126 else :
127127 # use the configured min/max allowable years if we
128128 # don't have any other bounds
129+ # TODO: make calendar-specific? these are min/max for gregorian
129130 min_year = self .MIN_ALLOWABLE_YEAR
130131 max_year = self .MAX_ALLOWABLE_YEAR
131132
@@ -166,7 +167,7 @@ def calculate_earliest_latest(self, year, month, day):
166167 else :
167168 # if we have no day or partial day, calculate min / max
168169 min_day = 1 # is min day ever anything other than 1 ?
169- rel_year = year if year and isinstance (year , int ) else None
170+ rel_year = year if year and isinstance (year , int ) else max_year
170171 # use month if it is an integer; otherwise use previusly determined
171172 # max month (which may not be 12 depending if partially unknown)
172173 rel_month = month if month and isinstance (month , int ) else latest_month
@@ -473,27 +474,29 @@ def duration(self) -> Timedelta | UnDelta:
473474 if self .precision == DatePrecision .DAY :
474475 return ONE_DAY
475476
477+ # if year is unknown or partially unknown, month and year duration both need to calculate for
478+ # variant years (leap year, non-leap year), since length may vary
479+ possible_years = [self .earliest .year ]
480+ if self .is_partially_known ("year" ):
481+ # if year is partially known (e.g. 191X), get all possible years in range
482+ # TODO: refactor into a function/property; combine/extract from missing digit min/max method
483+ possible_years = [
484+ int (str (self .year ).replace (self .MISSING_DIGIT , str (digit )))
485+ for digit in range (0 , 10 )
486+ ]
487+ # TODO: once this is working, make more efficient by only getting representative years from the calendar
488+ elif not self .known_year : # completely unknown year
489+ # TODO: should leap-year specific logic shift to the calendars,
490+ # since it works differently depending on the calendar?
491+ possible_years = [
492+ self .calendar_converter .LEAP_YEAR ,
493+ self .calendar_converter .NON_LEAP_YEAR ,
494+ ]
495+ possible_max_days = None
496+
476497 # if precision is month and year is unknown,
477498 # calculate month duration within a single year (not min/max)
478499 if self .precision == DatePrecision .MONTH :
479- latest = self .latest
480- # if year is unknown, calculate month duration in
481- # leap year and non-leap year, in case length varies
482- if not self .known_year :
483- # TODO: should leap-year specific logic shift to the calendars,
484- # since it works differently depending on the calendar?
485- possible_years = [
486- self .calendar_converter .LEAP_YEAR ,
487- self .calendar_converter .NON_LEAP_YEAR ,
488- ]
489- # TODO: handle partially known years like 191X,
490- # switch to representative years (depends on calendar)
491- # (to be implemented as part of ambiguous year duration)
492- else :
493- # otherwise, get possible durations for all possible months
494- # for a known year
495- possible_years = [self .earliest .year ]
496-
497500 # for every possible month and year, get max days for that month,
498501 possible_max_days = set ()
499502 # appease mypy, which says month values could be None here;
@@ -506,23 +509,45 @@ def duration(self) -> Timedelta | UnDelta:
506509 self .calendar_converter .max_day (year , possible_month )
507510 )
508511
509- # if there is more than one possible value for month length,
510- # whether due to leap year / non-leap year or ambiguous month,
511- # return an uncertain delta
512- if len (possible_max_days ) > 1 :
513- return UnDelta (* possible_max_days )
514-
515- # otherwise, calculate timedelta normally based on maximum day
516- max_day = list (possible_max_days )[0 ]
517- latest = Date (self .earliest .year , self .earliest .month , max_day )
512+ # if precision is year but year is unknown, return an uncertain delta
513+ elif self .precision == DatePrecision .YEAR :
514+ possible_max_days = set ()
515+ # this is currently hebrew-specific due to the way the start/end of year wraps for that calendar
516+ try :
517+ possible_max_days = set (
518+ [self .calendar_converter .days_in_year (y ) for y in possible_years ]
519+ )
520+ except NotImplementedError :
521+ pass
522+
523+ if not possible_max_days :
524+ for year in possible_years :
525+ # TODO: shift logic to calendars for parity with Hebrew?
526+ year_start = Date (
527+ * self .calendar_converter .to_gregorian (
528+ year , self .calendar_converter .min_month (), 1
529+ )
530+ )
531+ last_month = self .calendar_converter .max_month (year )
532+ year_end = Date (
533+ * self .calendar_converter .to_gregorian (
534+ year ,
535+ last_month ,
536+ self .calendar_converter .max_day (year , last_month ),
537+ )
538+ )
518539
519- return latest - self .earliest + ONE_DAY
540+ year_days = (year_end - year_start ).days + 1
541+ possible_max_days .add (year_days )
520542
521- # TODO: handle year precision + unknown/partially known year
522- # (will be handled in separate branch)
543+ # if there is more than one possible value for number of days
544+ # due to range including lear year / non-leap year, return an uncertain delta
545+ if possible_max_days and len (possible_max_days ) > 1 :
546+ return UnDelta (* possible_max_days )
547+ else :
548+ return Timedelta (possible_max_days .pop ())
523549
524- # otherwise, calculate based on earliest/latest range
525- # subtract earliest from latest and add a day to count start day
550+ # otherwise, subtract earliest from latest and add a day to include start day in the count
526551 return self .latest - self .earliest + ONE_DAY
527552
528553 def _missing_digit_minmax (
0 commit comments