diff --git a/tests/test_ranges.py b/tests/test_ranges.py index b27ecc7..d83fbdf 100644 --- a/tests/test_ranges.py +++ b/tests/test_ranges.py @@ -865,7 +865,7 @@ def test_respects_other_range_boundaries(self, other): def test_doesnt_extend_union(self): """ - A union of ranges should be longer than the sum of it's parts. + A union of ranges should not be longer than the sum of it's parts. """ # This is a weird test to include, it is added because this feels like an # obvious risk with the implementation I have used. @@ -886,6 +886,118 @@ def test_finite_range(self): assert 3 in subject + class TestDifference: + def test_paco(self): + r1 = ranges.FiniteDateRange( + start=datetime.date(2024, 10, 1), + end=datetime.date(2024, 10, 30), + ) + r2 = ranges.FiniteDateRange( + start=datetime.date(2024, 10, 5), + end=datetime.date(2024, 10, 15), + ) + r3 = ranges.FiniteDateRange( + start=datetime.date(2024, 12, 1), + end=datetime.date(2024, 12, 30), + ) + f1 = ranges.FiniteRangeSet([r1]) + f2 = ranges.FiniteRangeSet([r2]) + f3 = ranges.FiniteRangeSet([r3]) + r1.difference(r2) + r1.difference(r3) + + f1-f2 + + + + def test_range_sets_overlap(self): + range_set = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 10, 1), + end=datetime.date(2024, 10, 31), + ) + ] + ) + other = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 10, 1), + end=datetime.date(2024, 10, 15), + ) + ] + ) + difference = range_set.difference(other) + + assert difference == ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 10, 15), + end=datetime.date(2024, 10, 31), + ) + ] + ) + + def test_range_sets_overlap_multiple_ranges(self): + range_set = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 1, 1), + end=datetime.date(2024, 2, 28), + ), + ranges.FiniteDateRange( + start=datetime.date(2024, 5, 1), + end=datetime.date(2024, 6, 30), + ), + ] + ) + other = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 1, 1), + end=datetime.date(2024, 2, 15), + ), + ranges.FiniteDateRange( + start=datetime.date(2024, 5, 15), + end=datetime.date(2024, 6, 30), + ) + ] + ) + difference = range_set.difference(other) + + assert difference == ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 2, 15), + end=datetime.date(2024, 2, 28), + ), + ranges.FiniteDateRange( + start=datetime.date(2024, 5,1), + end=datetime.date(2024, 5, 15), + ), + ] + ) + + def test_difference_does_not_overlap(self): + range_set = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 1, 1), + end=datetime.date(2024, 1, 31), + ) + ] + ) + other = ranges.FiniteRangeSet( + [ + ranges.FiniteDateRange( + start=datetime.date(2024, 3, 1), + end=datetime.date(2024, 3, 31), + ) + ] + ) + difference = range_set.difference(other) + assert difference == range_set + class TestAsFiniteDatetimePeriods: def test_converts(self): diff --git a/xocto/ranges.py b/xocto/ranges.py index 7286b9c..7182ed4 100644 --- a/xocto/ranges.py +++ b/xocto/ranges.py @@ -876,9 +876,49 @@ def intersection(self, other: Range[datetime.date]) -> Optional["FiniteDateRange if base_intersection is None: return None - assert base_intersection.boundaries == RangeBoundaries.INCLUSIVE_INCLUSIVE return FiniteDateRange(base_intersection.start, base_intersection.end) + def difference(self, other: Range[datetime.date]) -> Optional["FiniteDateRange"]: + """ + Difference between two FiniteDateRanges should produce a FiniteDateRange. + """ + if self.is_disjoint(other): + return self + + boundaries = RangeBoundaries.INCLUSIVE_INCLUSIVE + + if (self.start is None and other.start is not None) or ( + self.start is not None + and other.start is not None + and not other._is_inside_left_bound(self.start) + and self._is_inside_left_bound(other.start) + ): + lower_part: Optional["Range[T]"] = Range( + self.start, other.start, boundaries=boundaries + ) + else: + lower_part = None + + if (self.end is None and other.end is not None) or ( + self.end is not None + and other.end is not None + and not other._is_inside_right_bound(self.end) + and self._is_inside_right_bound(other.end) + ): + upper_part: Optional["Range[T]"] = Range( + other.end, self.end, boundaries=boundaries + ) + else: + upper_part = None + + if lower_part is None and upper_part is None: + return None + elif lower_part is not None and upper_part is not None: + return RangeSet([lower_part, upper_part]) + else: + return lower_part or upper_part + + def union(self, other: Range[datetime.date]) -> Optional["FiniteDateRange"]: """ Unions between two FiniteDateRanges should produce a FiniteDateRange.