Skip to content

Commit 1b7ee68

Browse files
committed
Add comparison checks for fully unknown years
1 parent 378e082 commit 1b7ee68

File tree

2 files changed

+77
-2
lines changed

2 files changed

+77
-2
lines changed

src/undate/undate.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ def __eq__(self, other: object) -> bool:
335335
def __lt__(self, other: object) -> bool:
336336
other = self._comparison_type(other)
337337

338+
# if either date has a completely unknown year, then we can't compare
339+
if self.unknown_year or other.unknown_year:
340+
return False
341+
338342
# if this date ends before the other date starts,
339343
# return true (this date is earlier, so it is less)
340344
if self.latest < other.earliest:
@@ -370,9 +374,20 @@ def __gt__(self, other: object) -> bool:
370374
# define gt ourselves so we can support > comparison with datetime.date,
371375
# but rely on existing less than implementation.
372376
# strictly greater than must rule out equals
377+
378+
# if either date has a completely unknown year, then we can't compare
379+
# NOTE: this means that gt and lt will both be false when comparing
380+
# with a date with an unknown year...
381+
if self.unknown_year or isinstance(other, Undate) and other.unknown_year:
382+
return False
383+
373384
return not (self < other or self == other)
374385

375386
def __le__(self, other: object) -> bool:
387+
# if either date has a completely unknown year, then we can't compare
388+
if self.unknown_year or isinstance(other, Undate) and other.unknown_year:
389+
return False
390+
376391
return self == other or self < other
377392

378393
def __contains__(self, other: object) -> bool:
@@ -383,6 +398,10 @@ def __contains__(self, other: object) -> bool:
383398
if self == other:
384399
return False
385400

401+
# if either date has a completely unknown year, then we can't determine
402+
if self.unknown_year or other.unknown_year:
403+
return False
404+
386405
return all(
387406
[
388407
self.earliest <= other.earliest,
@@ -419,19 +438,30 @@ def to_undate(cls, other: object) -> "Undate":
419438

420439
@property
421440
def known_year(self) -> bool:
441+
"year is fully known"
422442
return self.is_known("year")
423443

444+
@property
445+
def unknown_year(self) -> bool:
446+
"year is completely unknown"
447+
return self.is_unknown("year")
448+
424449
def is_known(self, part: str) -> bool:
425-
"""Check if a part of the date (year, month, day) is known.
450+
"""Check if a part of the date (year, month, day) is fully known.
426451
Returns False if unknown or only partially known."""
427452
# TODO: should we use constants or enum for values?
428453

429454
# if we have an integer, then consider the date known
430455
# if we have a string, then it is only partially known; return false
431456
return isinstance(self.initial_values[part], int)
432457

458+
def is_unknown(self, part: str) -> bool:
459+
"""Check if a part of the date (year, month, day) is completely unknown."""
460+
return self.initial_values.get(part) is None
461+
433462
def is_partially_known(self, part: str) -> bool:
434-
# TODO: should XX / XXXX really be considered partially known? other code seems to assume this, so we'll preserve the behavior
463+
# TODO: should XX / XXXX really be considered partially known?
464+
# other code seems to assume this, so we'll preserve the behavior
435465
return isinstance(self.initial_values[part], str)
436466
# and self.initial_values[part].replace(self.MISSING_DIGIT, "") != ""
437467

tests/test_undate.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,19 @@ def test_lte(self, earlier, later):
310310
assert earlier <= later
311311
assert later >= earlier
312312

313+
def test_gt_lt_unknown_years(self):
314+
# unknown years cannot be compared on either side...
315+
year100 = Undate(100)
316+
some_january = Undate(month=1)
317+
assert not year100 < some_january
318+
assert not year100 <= some_january
319+
assert not year100 > some_january
320+
assert not year100 >= some_january
321+
assert not some_january < year100
322+
assert not some_january <= year100
323+
assert not some_january > year100
324+
assert not some_january >= year100
325+
313326
def test_lt_notimplemented(self):
314327
# how to compare mixed precision where dates overlap?
315328
# if the second date falls *within* earliest/latest,
@@ -362,6 +375,9 @@ def test_contains(self, date1, date2):
362375
(Undate(1980, "XX"), Undate(1980, "XX")),
363376
# - partially unknown month to unknown month
364377
(Undate(1801, "1X"), Undate(1801, "XX")),
378+
# fully unknown year
379+
(Undate(month=6, day=1), Undate(2022)),
380+
(Undate(1950), Undate(day=31)),
365381
]
366382

367383
@pytest.mark.parametrize("date1,date2", testdata_not_contains)
@@ -500,6 +516,7 @@ def test_partiallyknownyear_duration(self):
500516
assert Undate("XXX", calendar="Hebrew").duration().days == UnInt(353, 385)
501517

502518
def test_known_year(self):
519+
# known OR partially known
503520
assert Undate(2022).known_year is True
504521
assert Undate(month=2, day=5).known_year is False
505522
# partially known year is not known
@@ -521,6 +538,34 @@ def test_is_known_day(self):
521538
assert Undate(month=1, day="X5").is_known("day") is False
522539
assert Undate(month=1, day="XX").is_known("day") is False
523540

541+
def test_unknown_year(self):
542+
# fully unknown year
543+
assert Undate(month=2, day=5).unknown_year is True
544+
# known or partially known years = all false for unknown
545+
assert Undate(2022).unknown_year is False
546+
# partially known year is not unknown
547+
assert Undate("19XX").unknown_year is False
548+
# fully known string year should be known
549+
assert Undate("1900").unknown_year is False
550+
551+
def test_is_unknown_month(self):
552+
# fully unknown month
553+
assert Undate(2022).is_unknown("month") is True
554+
assert Undate(day=10).is_unknown("month") is True
555+
assert Undate(2022, 2).is_unknown("month") is False
556+
assert Undate(2022, "5").is_unknown("month") is False
557+
assert Undate(2022, "1X").is_unknown("month") is False
558+
assert Undate(2022, "XX").is_unknown("month") is False
559+
560+
def test_is_unknown_day(self):
561+
# fully unknown day
562+
assert Undate(1984).is_unknown("day") is True
563+
assert Undate(month=5).is_unknown("day") is True
564+
assert Undate(month=1, day=3).is_unknown("day") is False
565+
assert Undate(month=1, day="5").is_unknown("day") is False
566+
assert Undate(month=1, day="X5").is_unknown("day") is False
567+
assert Undate(month=1, day="XX").is_unknown("day") is False
568+
524569
def test_parse(self):
525570
assert Undate.parse("1984", "EDTF") == Undate(1984)
526571
assert Undate.parse("1984-04", "EDTF") == Undate(1984, 4)

0 commit comments

Comments
 (0)