Skip to content

Commit

Permalink
Add calendar converter base class and document how to add calendars
Browse files Browse the repository at this point in the history
  • Loading branch information
rlskoeser committed Nov 26, 2024
1 parent 6c6f09a commit 5cc19fd
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 16 deletions.
66 changes: 61 additions & 5 deletions src/undate/converters/base.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""
:class:`undate.converters.BaseDateConverter` provides a base class for
:class:`~undate.converters.BaseDateConverter` provides a base class for
implementing date converters, which can provide support for
parsing and generating dates in different formats and also converting
dates between different calendars.
parsing and generating dates in different formats.
The converter subclass :class:`undate.converters.BaseCalendarConverter`
provides additional functionaly needed for calendar conversion.
To add support for a new date format or calendar conversion:
To add support for a new date converter:
- Create a new file under ``undate/converters/``
- For converters with sufficient complexity, you may want to create a submodule;
Expand All @@ -18,6 +19,25 @@
The new subclass should be loaded automatically and included in the converters
returned by :meth:`BaseDateConverter.available_converters`
To add support for a new calendar converter:
- Create a new file under ``undate/converters/calendars/``
- For converters with sufficient complexity, you may want to create a submodule;
see ``undate.converters.calendars.hijri`` for an example.
- Extend ``BaseCalendarConverter`` and implement ``parse`` and ``to_string``
formatter methods as desired/appropriate for your converter as well as the
additional methods for ``max_month``, ``max_day``, and convertion ``to_gregorian``
calendar.
- Add unit tests for the new calendar logic under ``tests/test_converters/calendars/``
- Add the new calendar to the ``Calendar`` enum of supported calendars in
``undate/undate.py`` and confirm that the `get_converter` method loads your
calendar converter correctly (an existing unit test should cover this).
- Consider creating a notebook to demonstrate the use of the calendar
converter.
Calendar converter subclasses are also automatically loaded and included
in the list of available converters.
-------------------
"""

Expand Down Expand Up @@ -90,6 +110,42 @@ def available_converters(cls) -> Dict[str, Type["BaseDateConverter"]]:
"""
Dictionary of available converters keyed on name.
"""
return {c.name: c for c in cls.subclasses()} # type: ignore

@classmethod
def subclasses(cls) -> list[Type["BaseDateConverter"]]:
"""
List of available converters classes. Includes calendar convert
subclasses.
"""
# ensure undate converters are imported
cls.import_converters()
return {c.name: c for c in cls.__subclasses__()} # type: ignore

# find all direct subclasses, excluding base calendar converter
subclasses = cls.__subclasses__()
subclasses.remove(BaseCalendarConverter)
# add all subclasses of calendar converter base class
subclasses.extend(BaseCalendarConverter.__subclasses__())
return subclasses


class BaseCalendarConverter(BaseDateConverter):
"""Base class for calendar converters, with additional methods required
for calendars."""

#: Converter name. Subclasses must define a unique name.
name: str = "Base Calendar Converter"

def max_month(self, year: int) -> int:
"""Maximum month for this calendar for this year"""
raise NotImplementedError

def max_day(self, year: int, month: int) -> int:
"""maximum numeric day for the specified year and month in this calendar"""
raise NotImplementedError

def to_gregorian(self, year, month, day) -> tuple[int, int, int]:
"""Convert a date for this calendar specified by numeric year, month, and day,
into the Gregorian equivalent date. Should return a tuple of year, month, day.
"""
raise NotImplementedError
14 changes: 9 additions & 5 deletions src/undate/converters/calendars/gregorian.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from calendar import monthrange
from typing import Optional

from undate.converters.base import BaseDateConverter
from undate.converters.base import BaseCalendarConverter


class GregorianDateConverter(BaseDateConverter):
class GregorianDateConverter(BaseCalendarConverter):
"""
Converter class for Gregorian calendar.
Calendar onverter class for Gregorian calendar.
"""

#: converter name: Gregorian
Expand All @@ -20,7 +19,8 @@ def max_month(self, year: int) -> int:
"""Maximum month for this calendar for this year"""
return 12

def max_day(self, year: Optional[int] = None, month: Optional[int] = None) -> int:
def max_day(self, year: int, month: int) -> int:
"""maximum numeric day for the specified year and month in this calendar"""
# if month is known, use that to calculate
if month:
# if year is known, use it; otherwise use a known non-leap year
Expand All @@ -38,4 +38,8 @@ def max_day(self, year: Optional[int] = None, month: Optional[int] = None) -> in
return max_day

def to_gregorian(self, year, month, day) -> tuple[int, int, int]:
"""Convert a Hijri date, specified by year, month, and day,
to the Gregorian equivalent date. Returns a tuple of year, month, day.
"""

return (year, month, day)
4 changes: 2 additions & 2 deletions src/undate/converters/calendars/hijri/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
from convertdate import islamic # type: ignore
from lark.exceptions import UnexpectedCharacters

from undate.converters.base import BaseDateConverter
from undate.converters.base import BaseCalendarConverter
from undate.converters.calendars.hijri.parser import hijri_parser
from undate.converters.calendars.hijri.transformer import HijriDateTransformer
from undate.undate import Undate, UndateInterval


class HijriDateConverter(BaseDateConverter):
class HijriDateConverter(BaseCalendarConverter):
"""
Converter for Hijri / Islamic calendar.
Expand Down
4 changes: 2 additions & 2 deletions tests/test_converters/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_available_converters(self):

def test_converters_are_unique(self):
assert len(BaseDateConverter.available_converters()) == len(
BaseDateConverter.__subclasses__()
BaseDateConverter.subclasses()
), "Formatter names have to be unique."

def test_parse_not_implemented(self):
Expand Down Expand Up @@ -60,5 +60,5 @@ class ISO8601DateFormat2(BaseDateConverter):
name = "ISO8601" # duplicates existing formatter

assert len(BaseDateConverter.available_converters()) != len(
BaseDateConverter.__subclasses__()
BaseDateConverter.subclasses()
)
5 changes: 3 additions & 2 deletions tests/test_undate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from undate.converters.base import BaseDateConverter
from undate.converters.base import BaseCalendarConverter
from undate.date import DatePrecision, Timedelta
from undate.undate import Undate, UndateInterval, Calendar

Expand Down Expand Up @@ -573,4 +573,5 @@ def test_calendar_get_converter():
# calendar named in our calendar enum
for cal in Calendar:
converter = Calendar.get_converter(cal)
assert isinstance(converter, BaseDateConverter)
assert isinstance(converter, BaseCalendarConverter)
assert converter.name.lower() == cal.name.lower()

0 comments on commit 5cc19fd

Please sign in to comment.