Skip to content

Commit 5bb57c0

Browse files
committed
Clean up flagged by @coderabbitai with additional refactoring
1 parent 5b5746d commit 5bb57c0

4 files changed

Lines changed: 37 additions & 38 deletions

File tree

src/undate/converters/base.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
from functools import cache
4949
from typing import Dict, Type
5050

51+
from undate.date import Date
52+
5153
logger = logging.getLogger(__name__)
5254

5355

@@ -58,6 +60,10 @@ class BaseDateConverter:
5860
#: Converter name. Subclasses must define a unique name.
5961
name: str = "Base Converter"
6062

63+
# provisional...
64+
LEAP_YEAR = 0
65+
NON_LEAP_YEAR = 0
66+
6167
def parse(self, value: str):
6268
"""
6369
Parse a string and return an :class:`~undate.undate.Undate` or
@@ -163,8 +169,17 @@ def max_day(self, year: int, month: int) -> int:
163169
raise NotImplementedError
164170

165171
def days_in_year(self, year: int) -> int:
166-
"""number of days in the specified year in this calendar"""
167-
raise NotImplementedError
172+
"""Number of days in the specified year in this calendar. The default implementation
173+
uses min and max month and max day methods along with Gregorian conversion method
174+
to calculate the number of days in the specified year.
175+
"""
176+
year_start = Date(*self.to_gregorian(year, self.min_month(), 1))
177+
last_month = self.max_month(year)
178+
year_end = Date(
179+
*self.to_gregorian(year, last_month, self.max_day(year, last_month))
180+
)
181+
# add 1 because the difference doesn't include the end point
182+
return (year_end - year_start).days + 1
168183

169184
def to_gregorian(self, year, month, day) -> tuple[int, int, int]:
170185
"""Convert a date for this calendar specified by numeric year, month, and day,

src/undate/converters/calendars/seleucid.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ def to_gregorian(self, year: int, month: int, day: int) -> tuple[int, int, int]:
2222
logic with :attr:`SELEUCID_OFFSET`. Returns a tuple of year, month, day.
2323
"""
2424
return super().to_gregorian(year + self.SELEUCID_OFFSET, month, day)
25+
26+
def days_in_year(self, year: int) -> int:
27+
"""the number of days in the specified year for this calendar"""
28+
return super().days_in_year(year + self.SELEUCID_OFFSET)

src/undate/undate.py

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
# Pre 3.10 requires Union for multiple types, e.g. Union[int, None] instead of int | None
2020
from typing import Dict, Optional, Union
2121

22-
from undate.converters.base import BaseDateConverter
22+
from undate.converters.base import BaseCalendarConverter, BaseDateConverter
2323
from undate.date import ONE_DAY, Date, DatePrecision, Timedelta, UnDelta
2424

2525

@@ -32,10 +32,14 @@ class Calendar(StrEnum):
3232
SELEUCID = auto()
3333

3434
@staticmethod
35-
def get_converter(calendar):
35+
def get_converter(calendar) -> BaseCalendarConverter:
3636
# calendar converter must be available with a name matching
3737
# the title-case name of the calendar enum entry
3838
converter_cls = BaseDateConverter.available_converters()[calendar.value.title()]
39+
if not issubclass(converter_cls, BaseCalendarConverter):
40+
raise ValueError(
41+
f"Requested converter {converter_cls} is not a CalendarConverter"
42+
)
3943
return converter_cls()
4044

4145

@@ -492,13 +496,12 @@ def duration(self) -> Timedelta | UnDelta:
492496
self.calendar_converter.LEAP_YEAR,
493497
self.calendar_converter.NON_LEAP_YEAR,
494498
]
495-
possible_max_days = None
499+
possible_max_days = set()
496500

497501
# if precision is month and year is unknown,
498502
# calculate month duration within a single year (not min/max)
499503
if self.precision == DatePrecision.MONTH:
500504
# for every possible month and year, get max days for that month,
501-
possible_max_days = set()
502505
# appease mypy, which says month values could be None here;
503506
# Date object allows optional month, but earliest/latest initialization
504507
# should always be day-precision dates
@@ -511,40 +514,17 @@ def duration(self) -> Timedelta | UnDelta:
511514

512515
# if precision is year but year is unknown, return an uncertain delta
513516
elif self.precision == DatePrecision.YEAR:
514-
possible_max_days = set()
515517
# 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-
)
539-
540-
year_days = (year_end - year_start).days + 1
541-
possible_max_days.add(year_days)
518+
# with contextlib.suppress(NotImplementedError):
519+
possible_max_days = {
520+
self.calendar_converter.days_in_year(y) for y in possible_years
521+
}
542522

543523
# if there is more than one possible value for number of days
544524
# 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:
525+
if possible_max_days:
526+
if len(possible_max_days) > 1:
527+
return UnDelta(*possible_max_days)
548528
return Timedelta(possible_max_days.pop())
549529

550530
# otherwise, subtract earliest from latest and add a day to include start day in the count

tests/test_undate.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -448,9 +448,9 @@ def test_partiallyknownyear_duration(self):
448448
assert Undate("108X", calendar="Islamic").duration().days == UnInt(354, 355)
449449
# NOTE: completely unknown years is not yet supported for other calendars, will cause an error
450450
# assert Undate("XXXX", calendar="Islamic").duration().days == UnInt(354, 355)
451-
#
452-
print(Undate("536X", calendar="Hebrew").duration())
453451
assert Undate("536X", calendar="Hebrew").duration().days == UnInt(353, 385)
452+
# different set of years could vary
453+
assert Undate("53X2", calendar="Hebrew").duration().days == UnInt(354, 385)
454454

455455
def test_known_year(self):
456456
assert Undate(2022).known_year is True

0 commit comments

Comments
 (0)