Skip to content

Commit

Permalink
Fixed comparison operators for Range when dealing with empty and unbo…
Browse files Browse the repository at this point in the history
…unded ranges. Refactored Range.intersection
  • Loading branch information
runfalk committed Mar 23, 2017
1 parent 11c24ed commit d55df71
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 26 deletions.
16 changes: 11 additions & 5 deletions spans/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,28 @@ def __setstate__(self, data):

def sane_total_ordering(cls):
def __ge__(self, other):
lt = self.__lt__(other)
if lt is NotImplemented:
gt = self.__gt__(other)
if gt is NotImplemented:
return NotImplemented
elif gt:
return True

return not lt
eq = self.__eq__(other)
if eq is NotImplemented:
return NotImplemented
return eq

def __le__(self, other):
lt = self.__lt__(other)
if lt is NotImplemented:
return NotImplemented
elif lt:
return True

eq = self.__eq__(other)
if eq is NotImplemented:
return NotImplemented

return lt or eq
return eq

def __gt__(self, other):
le = __le__(self, other)
Expand Down
42 changes: 32 additions & 10 deletions spans/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,37 @@ def __eq__(self, other):
def __lt__(self, other):
if not self.is_valid_range(other):
return NotImplemented
# When dealing with empty ranges there is not such thing as order
elif not self or not other:
return False
elif self.lower == other.lower:
# If lower are equal for both, we need to consider the inclusiveness
# of both bounds
if self.lower_inc != other.lower_inc:
return self.lower_inc
# If upper bounds are the same, self can only be smaller if it is
# not inclusive and other is
elif self.upper == other.upper:
return not self.upper_inc and other.upper_inc
elif self.upper_inf or other.upper_inf:
return other.upper_inf
else:
# We need consider the case when an upper bound is infinite
return self.upper < other.upper
elif self.lower_inf or other.lower_inf:
# If self.lower is unbound (infinite) it will always be smaller,
# unless other.lower is also unbound
return not other.lower_inf
else:
return self.lower < other.lower

def __gt__(self, other):
if not self.is_valid_range(other):
return NotImplemented
elif not self or not other:
return False
return not (self < other or self == other)

def __nonzero__(self):
return not self._range.empty

Expand Down Expand Up @@ -351,6 +372,10 @@ def overlap(self, other):
See also :meth:`~spans.types.Range.intersection`.
"""

# Special case for empty ranges
if not self or not other:
return True

if self < other:
a, b = self, other
else:
Expand Down Expand Up @@ -506,19 +531,16 @@ def intersection(self, other):
:return: A new range that is the intersection between this and `other`.
"""

# Handle ranges not intersecting
if not self or not other or not self.overlap(other):
return self.empty()
elif self.contains(other):
return other
elif self.within(other):
return self

out = self
if not self.startsafter(other):
out = out.replace(lower=other.lower, lower_inc=other.lower_inc)
if not self.endsbefore(other):
return out.replace(upper=other.upper, upper_inc=other.upper_inc)
return out
lower_end_span = self if self.startsafter(other) else other
upper_end_span = self if self.endsbefore(other) else other

return lower_end_span.replace(
upper=upper_end_span.upper,
upper_inc=upper_end_span.upper_inc)

def startswith(self, other):
"""
Expand Down
52 changes: 41 additions & 11 deletions tests/test_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,22 +92,51 @@ def test_equality():
assert not intrange() == None


def test_less_than():
assert intrange(1, 5) < intrange(2, 5)
assert intrange(1, 4) < intrange(1, 5)
assert not intrange(1, 5) < intrange(1, 5)
assert intrange(1, 5) < intrange(1, 5, upper_inc=True)
assert not intrange(1, 5, lower_inc=False) < intrange(1, 5)
@pytest.mark.parametrize("a, b", [
(floatrange(1.0, 5.0), floatrange(2.0, 5.0)),
(floatrange(1.0, 4.0), floatrange(1.0, 5.0)),
(floatrange(1.0, 5.0), floatrange(1.0, 5.0, upper_inc=True)),
(floatrange(1.0, 5.0), floatrange(1.0)),
(floatrange(upper=5.0), floatrange(1.0, 5.0)),
])
def test_less_than(a, b):
assert a < b
assert not b < a

assert intrange(1, 5) <= intrange(1, 5)
assert intrange(1, 4) <= intrange(1, 5)
assert not intrange(2, 5) <= intrange(1, 5)

@pytest.mark.parametrize("a, b", [
(floatrange(1.0, 5.0), floatrange(1.0, 5.0)),
(floatrange(1.0, 4.0), floatrange(1.0, 5.0)),
])
def test_less_equal(a, b):
assert a <= b


@pytest.mark.parametrize("a, b", [
(floatrange.empty(), floatrange.empty()),
(floatrange.empty(), floatrange(1.0)),
(floatrange(upper=-1.0), floatrange.empty()),
])
def test_empty_comparison(a, b):
assert not a < b
assert not a > b


@pytest.mark.parametrize("a, b", [
(intrange(), floatrange()),
(intrange(), None),
])
@pytest.mark.parametrize("op", [
"__lt__",
"__le__",
"__gt__",
"__ge__",
])
def test_comparison_operator_type_checks(a, b, op):
# Hack used to work around version differences between Python 2 and 3
# Python 2 has its own idea of how objects compare to each other.
# Python 3 raises type error when an operation is not implemented
assert intrange().__lt__(floatrange()) is NotImplemented
assert intrange().__le__(floatrange()) is NotImplemented
assert getattr(a, op)(b) is NotImplemented


def test_greater_than():
Expand All @@ -116,6 +145,7 @@ def test_greater_than():
assert not intrange(1, 5) > intrange(1, 5)
assert intrange(1, 5, upper_inc=True) > intrange(1, 5)
assert intrange(1, 5, lower_inc=False) > intrange(1, 5)
assert intrange(2) > intrange(1, 5)

assert intrange(1, 5) >= intrange(1, 5)
assert intrange(1, 5) >= intrange(1, 4)
Expand Down

0 comments on commit d55df71

Please sign in to comment.