Skip to content

Commit

Permalink
Improve comparison/contains support for datetime.date
Browse files Browse the repository at this point in the history
  • Loading branch information
rlskoeser committed Oct 27, 2023
1 parent 8cfa8f0 commit e017a49
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 21 deletions.
38 changes: 24 additions & 14 deletions src/undate/undate.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,9 @@ def __repr__(self) -> str:
def __eq__(self, other: Union["Undate", datetime.date]) -> bool:
# Note: assumes label differences don't matter for comparing dates

# support comparison with datetime date ONLY for full day precision
# only a day-precision fully known undate can be equal to a datetime.date
if isinstance(other, datetime.date):
if self.precision == DatePrecision.DAY:
return self.earliest == other
else:
raise NotImplementedError(
"Equality comparision with datetime.date not supported for %s precision"
% self.precision
)
return self.earliest == other and self.latest == other

# check for apparent equality
looks_equal = (
Expand All @@ -209,8 +203,10 @@ def __eq__(self, other: Union["Undate", datetime.date]) -> bool:
return False
return looks_equal

def __lt__(self, other: "Undate") -> bool:
# TODO: support datetime.date (?)
def __lt__(self, other: Union["Undate", datetime.date]) -> bool:
# support datetime.date by converting to undate
if isinstance(other, datetime.date):
other = Undate.from_datetime_date(other)

# if this date ends before the other date starts,
# return true (this date is earlier, so it is less)
Expand All @@ -233,24 +229,38 @@ def __lt__(self, other: "Undate") -> bool:
# for any other case (i.e., self == other), return false
return False

def __le__(self, other: "Undate") -> bool:
def __gt__(self, other: Union["Undate", datetime.date]) -> bool:
# define gt ourselves so we can support > comparison with datetime.date,
# but rely on existing less than implementation.
# strictly greater than must rule out equals
return not (self < other or self == other)

def __le__(self, other: Union["Undate", datetime.date]) -> bool:
return self == other or self < other

def __contains__(self, other: "Undate") -> bool:
def __contains__(self, other: Union["Undate", datetime.date]) -> bool:
# if the two dates are strictly equal, don't consider
# either one as containing the other

# support comparison with datetime by converting to undate
if isinstance(other, datetime.date):
other = Undate.from_datetime_date(other)

if self == other:
return False

# TODO: support datetime.date ?

return (
self.earliest <= other.earliest
and self.latest >= other.latest
# is precision sufficient for comparing partially known dates?
and self.precision > other.precision
)

@staticmethod
def from_datetime_date(dt_date):
"""Initialize an :class:`Undate` object from a :class:`datetime.date`"""
return Undate(dt_date.year, dt_date.month, dt_date.day)

@property
def known_year(self) -> bool:
return self.is_known("year")
Expand Down
30 changes: 23 additions & 7 deletions tests/test_undate.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ def test_invalid_date(self):
with pytest.raises(ValueError):
Undate(1990, 22)

def test_from_datetime_date(self):
undate_from_date = Undate.from_datetime_date(date(2001, 3, 5))
assert isinstance(undate_from_date, Undate)
assert undate_from_date == Undate(2001, 3, 5)

def test_eq(self):
assert Undate(2022) == Undate(2022)
assert Undate(2022, 10) == Undate(2022, 10)
Expand All @@ -136,14 +141,11 @@ def test_eq_datetime_date(self):
# support comparisons with datetime objects for full day-precision
assert Undate(2022, 10, 1) == date(2022, 10, 1)
assert Undate(2022, 10, 1) != date(2022, 10, 2)
assert Undate(2022, 10, 1) != date(2021, 10, 1)
assert Undate(1980, 10, 1) != date(2022, 10, 1)

# error on attempt to compare when precision is not known to the day
with pytest.raises(
NotImplementedError,
match="Equality comparision with datetime.date not supported for YEAR precision",
):
assert Undate(2022) == date(2022, 10, 1)
# other date precisions are not equal
assert Undate(2022) != date(2022, 10, 1)
assert Undate(2022, 10) != date(2022, 10, 1)

def test_not_eq(self):
assert Undate(2022) != Undate(2023)
Expand All @@ -169,6 +171,9 @@ def test_not_eq(self):
# partially known digits where comparison is possible
(Undate("19XX"), Undate("20XX")),
(Undate(1900, "0X"), Undate(1900, "1X")),
# compare with datetime.date objects
(Undate("19XX"), date(2020, 1, 1)),
(Undate(1991, 1), date(1992, 3, 4)),
]

@pytest.mark.parametrize("earlier,later", testdata_lt_gt)
Expand All @@ -183,13 +188,18 @@ def test_lt(self, earlier, later):
(Undate(1601), Undate(1601)),
(Undate(1991, 1), Undate(1991, 1)),
(Undate(1492, 5, 3), Undate(1492, 5, 3)),
# compare with datetime.date also
(Undate(1492, 5, 3), date(1492, 5, 3)),
]
)

def test_lt_when_eq(self):
# strict less than / greater should return false when equal
assert not Undate(1900) > Undate(1900)
assert not Undate(1900) < Undate(1900)
# same for datetime.date
assert not Undate(1903, 1, 5) < date(1903, 1, 5)
assert not Undate(1903, 1, 5) > date(1903, 1, 5)

@pytest.mark.parametrize("earlier,later", testdata_lte_gte)
def test_lte(self, earlier, later):
Expand All @@ -214,6 +224,9 @@ def test_lt_notimplemented(self):
(Undate(2022, 1, 1), Undate(2022)),
(Undate(2022, 12, 31), Undate(2022)),
(Undate(2022, 6, 15), Undate(2022, 6)),
# support contains with datetime.date
(date(2022, 6, 1), Undate(2022)),
(date(2022, 6, 1), Undate(2022, 6)),
]

@pytest.mark.parametrize("date1,date2", testdata_contains)
Expand All @@ -225,6 +238,9 @@ def test_contains(self, date1, date2):
(Undate(1980), Undate(2020)),
(Undate(1980), Undate(2020, 6)),
(Undate(1980, 6), Undate(2020, 6)),
# support contains with datetime.date
(date(1980, 6, 1), Undate(2022)),
(date(3001, 6, 1), Undate(2022, 6)),
# partially known dates that are similar but same precision,
# so one does not contain the other
(Undate("199X"), Undate("19XX")),
Expand Down

0 comments on commit e017a49

Please sign in to comment.