Skip to content

Commit e6b96df

Browse files
committed
Refactor and test logic for possible years
1 parent 5bb57c0 commit e6b96df

2 files changed

Lines changed: 75 additions & 20 deletions

File tree

src/undate/undate.py

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests/test_undate.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,35 @@ def test_sorting(self):
393393
# someyear = Undate("1XXX")
394394
# assert sorted([d1991, someyear]) == [someyear, d1991]
395395

396+
def test_possible_years(self):
397+
assert Undate(1991).possible_years == [1991]
398+
assert Undate("190X").possible_years == range(1900, 1910)
399+
assert Undate("19XX").possible_years == range(1900, 2000)
400+
# uses step when missing digit is not last digit
401+
assert Undate("19X1").possible_years == range(1901, 1992, 10)
402+
assert Undate("2X25").possible_years == range(2025, 2926, 100)
403+
assert Undate("1XXX").possible_years == range(1000, 2000)
404+
# completely unknown year raises value error, because the range is not useful
405+
with pytest.raises(
406+
ValueError, match="cannot be returned for completely unknown year"
407+
):
408+
Undate("XXXX").possible_years
409+
410+
def test_representative_years(self):
411+
assert Undate("1991").representative_years == [1991]
412+
assert Undate("190X").representative_years == [
413+
1900,
414+
1901,
415+
1902,
416+
1903,
417+
1904,
418+
1905,
419+
1906,
420+
1907,
421+
1908,
422+
1909,
423+
]
424+
396425
def test_duration(self):
397426
day_duration = Undate(2022, 11, 7).duration()
398427
assert isinstance(day_duration, Timedelta)

0 commit comments

Comments
 (0)