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