Skip to content

Commit 5cc19fd

Browse files
committed
Add calendar converter base class and document how to add calendars
1 parent 6c6f09a commit 5cc19fd

File tree

5 files changed

+77
-16
lines changed

5 files changed

+77
-16
lines changed

src/undate/converters/base.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""
2-
:class:`undate.converters.BaseDateConverter` provides a base class for
2+
:class:`~undate.converters.BaseDateConverter` provides a base class for
33
implementing date converters, which can provide support for
4-
parsing and generating dates in different formats and also converting
5-
dates between different calendars.
4+
parsing and generating dates in different formats.
5+
The converter subclass :class:`undate.converters.BaseCalendarConverter`
6+
provides additional functionaly needed for calendar conversion.
67
7-
To add support for a new date format or calendar conversion:
8+
To add support for a new date converter:
89
910
- Create a new file under ``undate/converters/``
1011
- For converters with sufficient complexity, you may want to create a submodule;
@@ -18,6 +19,25 @@
1819
The new subclass should be loaded automatically and included in the converters
1920
returned by :meth:`BaseDateConverter.available_converters`
2021
22+
To add support for a new calendar converter:
23+
24+
- Create a new file under ``undate/converters/calendars/``
25+
- For converters with sufficient complexity, you may want to create a submodule;
26+
see ``undate.converters.calendars.hijri`` for an example.
27+
- Extend ``BaseCalendarConverter`` and implement ``parse`` and ``to_string``
28+
formatter methods as desired/appropriate for your converter as well as the
29+
additional methods for ``max_month``, ``max_day``, and convertion ``to_gregorian``
30+
calendar.
31+
- Add unit tests for the new calendar logic under ``tests/test_converters/calendars/``
32+
- Add the new calendar to the ``Calendar`` enum of supported calendars in
33+
``undate/undate.py`` and confirm that the `get_converter` method loads your
34+
calendar converter correctly (an existing unit test should cover this).
35+
- Consider creating a notebook to demonstrate the use of the calendar
36+
converter.
37+
38+
Calendar converter subclasses are also automatically loaded and included
39+
in the list of available converters.
40+
2141
-------------------
2242
"""
2343

@@ -90,6 +110,42 @@ def available_converters(cls) -> Dict[str, Type["BaseDateConverter"]]:
90110
"""
91111
Dictionary of available converters keyed on name.
92112
"""
113+
return {c.name: c for c in cls.subclasses()} # type: ignore
114+
115+
@classmethod
116+
def subclasses(cls) -> list[Type["BaseDateConverter"]]:
117+
"""
118+
List of available converters classes. Includes calendar convert
119+
subclasses.
120+
"""
93121
# ensure undate converters are imported
94122
cls.import_converters()
95-
return {c.name: c for c in cls.__subclasses__()} # type: ignore
123+
124+
# find all direct subclasses, excluding base calendar converter
125+
subclasses = cls.__subclasses__()
126+
subclasses.remove(BaseCalendarConverter)
127+
# add all subclasses of calendar converter base class
128+
subclasses.extend(BaseCalendarConverter.__subclasses__())
129+
return subclasses
130+
131+
132+
class BaseCalendarConverter(BaseDateConverter):
133+
"""Base class for calendar converters, with additional methods required
134+
for calendars."""
135+
136+
#: Converter name. Subclasses must define a unique name.
137+
name: str = "Base Calendar Converter"
138+
139+
def max_month(self, year: int) -> int:
140+
"""Maximum month for this calendar for this year"""
141+
raise NotImplementedError
142+
143+
def max_day(self, year: int, month: int) -> int:
144+
"""maximum numeric day for the specified year and month in this calendar"""
145+
raise NotImplementedError
146+
147+
def to_gregorian(self, year, month, day) -> tuple[int, int, int]:
148+
"""Convert a date for this calendar specified by numeric year, month, and day,
149+
into the Gregorian equivalent date. Should return a tuple of year, month, day.
150+
"""
151+
raise NotImplementedError

src/undate/converters/calendars/gregorian.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from calendar import monthrange
2-
from typing import Optional
32

4-
from undate.converters.base import BaseDateConverter
3+
from undate.converters.base import BaseCalendarConverter
54

65

7-
class GregorianDateConverter(BaseDateConverter):
6+
class GregorianDateConverter(BaseCalendarConverter):
87
"""
9-
Converter class for Gregorian calendar.
8+
Calendar onverter class for Gregorian calendar.
109
"""
1110

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

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

4040
def to_gregorian(self, year, month, day) -> tuple[int, int, int]:
41+
"""Convert a Hijri date, specified by year, month, and day,
42+
to the Gregorian equivalent date. Returns a tuple of year, month, day.
43+
"""
44+
4145
return (year, month, day)

src/undate/converters/calendars/hijri/converter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from convertdate import islamic # type: ignore
44
from lark.exceptions import UnexpectedCharacters
55

6-
from undate.converters.base import BaseDateConverter
6+
from undate.converters.base import BaseCalendarConverter
77
from undate.converters.calendars.hijri.parser import hijri_parser
88
from undate.converters.calendars.hijri.transformer import HijriDateTransformer
99
from undate.undate import Undate, UndateInterval
1010

1111

12-
class HijriDateConverter(BaseDateConverter):
12+
class HijriDateConverter(BaseCalendarConverter):
1313
"""
1414
Converter for Hijri / Islamic calendar.
1515

tests/test_converters/test_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_available_converters(self):
1818

1919
def test_converters_are_unique(self):
2020
assert len(BaseDateConverter.available_converters()) == len(
21-
BaseDateConverter.__subclasses__()
21+
BaseDateConverter.subclasses()
2222
), "Formatter names have to be unique."
2323

2424
def test_parse_not_implemented(self):
@@ -60,5 +60,5 @@ class ISO8601DateFormat2(BaseDateConverter):
6060
name = "ISO8601" # duplicates existing formatter
6161

6262
assert len(BaseDateConverter.available_converters()) != len(
63-
BaseDateConverter.__subclasses__()
63+
BaseDateConverter.subclasses()
6464
)

tests/test_undate.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from undate.converters.base import BaseDateConverter
6+
from undate.converters.base import BaseCalendarConverter
77
from undate.date import DatePrecision, Timedelta
88
from undate.undate import Undate, UndateInterval, Calendar
99

@@ -573,4 +573,5 @@ def test_calendar_get_converter():
573573
# calendar named in our calendar enum
574574
for cal in Calendar:
575575
converter = Calendar.get_converter(cal)
576-
assert isinstance(converter, BaseDateConverter)
576+
assert isinstance(converter, BaseCalendarConverter)
577+
assert converter.name.lower() == cal.name.lower()

0 commit comments

Comments
 (0)