Skip to content

Commit 0fefcec

Browse files
committed
Move interval object and tests into separate files
1 parent ea45598 commit 0fefcec

16 files changed

Lines changed: 299 additions & 231 deletions

File tree

docs/undate/core.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
Undate objects
22
==============
33

4-
undates and undate intervals
4+
dates, intervals, and calendar
55
------------------------------
66

77
.. autoclass:: undate.undate.Undate
88
:members:
99

10-
.. autoclass:: undate.undate.UndateInterval
10+
.. autoclass:: undate.undate.Calendar
11+
:members:
12+
13+
.. autoclass:: undate.interval.UndateInterval
1114
:members:
1215

1316
date, timedelta, and date precision

src/undate/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
__version__ = "0.4.0.dev0"
22

33
from undate.date import DatePrecision
4-
from undate.undate import Undate, UndateInterval
4+
from undate.undate import Undate, Calendar
5+
from undate.interval import UndateInterval
56

6-
__all__ = ["Undate", "UndateInterval", "DatePrecision", "__version__"]
7+
__all__ = ["Undate", "UndateInterval", "Calendar", "DatePrecision", "__version__"]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from convertdate import hebrew # type: ignore
44
from lark.exceptions import UnexpectedCharacters
55

6+
from undate import Undate, UndateInterval
67
from undate.converters.base import BaseCalendarConverter
78
from undate.converters.calendars.hebrew.parser import hebrew_parser
89
from undate.converters.calendars.hebrew.transformer import HebrewDateTransformer
9-
from undate.undate import Undate, UndateInterval
1010

1111

1212
class HebrewDateConverter(BaseCalendarConverter):

src/undate/converters/calendars/hebrew/transformer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from lark import Transformer, Tree
22

3-
from undate.undate import Undate, Calendar
3+
from undate import Undate, Calendar
44

55

66
class HebrewUndate(Undate):

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from convertdate import islamic # type: ignore
44
from lark.exceptions import UnexpectedCharacters
55

6+
from undate import Undate, UndateInterval
67
from undate.converters.base import BaseCalendarConverter
78
from undate.converters.calendars.hijri.parser import hijri_parser
89
from undate.converters.calendars.hijri.transformer import HijriDateTransformer
9-
from undate.undate import Undate, UndateInterval
1010

1111

1212
class HijriDateConverter(BaseCalendarConverter):

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from lark import Transformer, Tree
22

3-
from undate.undate import Undate, Calendar
3+
from undate import Undate, Calendar
44

55

66
class HijriUndate(Undate):

src/undate/converters/edtf/converter.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from lark.exceptions import UnexpectedCharacters
44

5+
from undate import Undate, UndateInterval
56
from undate.converters.base import BaseDateConverter
67
from undate.converters.edtf.parser import edtf_parser
78
from undate.converters.edtf.transformer import EDTFTransformer
89
from undate.date import DatePrecision
9-
from undate.undate import Undate, UndateInterval
10+
1011

1112
#: character for unspecified digits
1213
EDTF_UNSPECIFIED_DIGIT: str = "X"

src/undate/converters/edtf/transformer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from lark import Token, Transformer, Tree
22

3-
from undate.undate import Undate, UndateInterval
3+
from undate import Undate, UndateInterval
44

55

66
class EDTFTransformer(Transformer):

src/undate/converters/iso8601.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Dict, List, Union
22

3+
from undate import Undate, UndateInterval
34
from undate.converters.base import BaseDateConverter
4-
from undate.undate import Undate, UndateInterval
55

66

77
class ISO8601DateFormat(BaseDateConverter):

src/undate/interval.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import datetime
2+
3+
# Pre 3.10 requires Union for multiple types, e.g. Union[int, None] instead of int | None
4+
from typing import Optional, Union
5+
6+
7+
from undate import Undate
8+
from undate.date import ONE_DAY, ONE_YEAR, Timedelta
9+
from undate.converters.base import BaseDateConverter
10+
11+
12+
class UndateInterval:
13+
"""A date range between two uncertain dates.
14+
15+
:param earliest: Earliest undate
16+
:type earliest: `undate.Undate`
17+
:param latest: Latest undate
18+
:type latest: `undate.Undate`
19+
:param label: A string to label a specific undate interval, similar to labels of `undate.Undate`.
20+
:type label: `str`
21+
"""
22+
23+
# date range between two undates
24+
earliest: Union[Undate, None]
25+
latest: Union[Undate, None]
26+
label: Union[str, None]
27+
28+
# TODO: let's think about adding an optional precision / length /size field
29+
# using DatePrecision
30+
31+
def __init__(
32+
self,
33+
earliest: Optional[Undate] = None,
34+
latest: Optional[Undate] = None,
35+
label: Optional[str] = None,
36+
):
37+
# for now, assume takes two undate objects;
38+
# support conversion from datetime
39+
if earliest and not isinstance(earliest, Undate):
40+
# NOTE: some overlap with Undate._comparison_type method
41+
# maybe support conversion from other formats later
42+
if isinstance(earliest, datetime.date):
43+
earliest = Undate.from_datetime_date(earliest)
44+
else:
45+
raise ValueError(
46+
f"earliest date {earliest} cannot be converted to Undate"
47+
)
48+
if latest and not isinstance(latest, Undate):
49+
if isinstance(latest, datetime.date):
50+
latest = Undate.from_datetime_date(latest)
51+
else:
52+
raise ValueError(f"latest date {latest} cannot be converted to Undate")
53+
54+
# check that the interval is valid
55+
if latest and earliest and latest <= earliest:
56+
raise ValueError(f"invalid interval {earliest}-{latest}")
57+
58+
self.earliest = earliest
59+
self.latest = latest
60+
self.label = label
61+
62+
def __str__(self) -> str:
63+
# using EDTF syntax for open ranges
64+
return "%s/%s" % (self.earliest or "..", self.latest or "")
65+
66+
def format(self, format) -> str:
67+
"""format this undate interval as a string using the specified format;
68+
for now, only supports named converters"""
69+
converter_cls = BaseDateConverter.available_converters().get(format, None)
70+
print(f"converter_cls == {converter_cls}")
71+
if converter_cls:
72+
return converter_cls().to_string(self)
73+
74+
raise ValueError(f"Unsupported format '{format}'")
75+
76+
def __repr__(self) -> str:
77+
if self.label:
78+
return "<UndateInterval '%s' (%s)>" % (self.label, self)
79+
return "<UndateInterval %s>" % self
80+
81+
def __eq__(self, other) -> bool:
82+
# consider interval equal if both dates are equal
83+
return self.earliest == other.earliest and self.latest == other.latest
84+
85+
def duration(self) -> Timedelta:
86+
"""Calculate the duration between two undates.
87+
Note that durations are inclusive (i.e., a closed interval), and
88+
include both the earliest and latest date rather than the difference
89+
between them.
90+
91+
:returns: A duration
92+
:rtype: Timedelta
93+
"""
94+
# what is the duration of this date range?
95+
96+
# if range is open-ended, can't calculate
97+
if self.earliest is None or self.latest is None:
98+
return NotImplemented
99+
100+
# if both years are known, subtract end of range from beginning of start
101+
if self.latest.known_year and self.earliest.known_year:
102+
return self.latest.latest - self.earliest.earliest + ONE_DAY
103+
104+
# if neither year is known...
105+
elif not self.latest.known_year and not self.earliest.known_year:
106+
# under what circumstances can we assume that if both years
107+
# are unknown the dates are in the same year or sequential?
108+
duration = self.latest.earliest - self.earliest.earliest
109+
# if we get a negative, we've wrapped from end of one year
110+
# to the beginning of the next;
111+
# recalculate assuming second date is in the subsequent year
112+
if duration.days < 0:
113+
end = self.latest.earliest + ONE_YEAR
114+
duration = end - self.earliest.earliest
115+
116+
# add the additional day *after* checking for a negative
117+
# or after recalculating with adjusted year
118+
duration += ONE_DAY
119+
120+
return duration
121+
122+
else:
123+
# is there any meaningful way to calculate duration
124+
# if one year is known and the other is not?
125+
raise NotImplementedError

0 commit comments

Comments
 (0)