Repository: runfalk/spans

Issue:
Issue with floatrange intersection
Hello,

There seems to be a bug with floatrange intersection (or maybe I do not understand it correctly).
It can be observed when first range has same upper value as the lower value of the second range, but lower value of the second range is not included.

Here is an example:
`floatrange(0.0, 5.0, upper_inc=True).intersection(floatrange(5.0, lower_inc=False))`

Which results in:
`floatrange(5.0, 5.0, lower_inc=False, upper_inc=True)`

As I understand, expected result should be an empty set, because 5.0 is not included in the first range, so it can also not be an intersection.
The result is also misleading, because same number is both included and not included (a Schrödinger range?).

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index fe1cb7d..a297ec7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ build/
 dist/
 *venv/
 doc/_build/
+
+.vscode/
diff --git a/spans/types.py b/spans/types.py
index 8f1022c..45758ab 100644
--- a/spans/types.py
+++ b/spans/types.py
@@ -60,13 +60,11 @@ class _Bound(PartialOrderingMixin):
             if self.is_lower:
                 if self.inc:
                     return other.is_lower ^ other.inc
-                else:
-                    return not other.is_lower and other.inc
+                return False
             else:
                 if self.inc:
                     return other.is_lower and not other.inc
-                else:
-                    return other.is_lower or other.inc
+                return other.is_lower or other.inc
         else:
             return False
 
@@ -154,6 +152,11 @@ class Range(PartialOrderingMixin, PicklableSlotMixin):
 
         if upper is None and upper_inc:
             raise ValueError("Upper bound can not be inclusive when infinite")
+        
+        # Handle normalization to empty ranges
+        if lower is not None and lower == upper and not (lower_inc and upper_inc):
+            self._range = _empty_internal_range
+            return
 
         self._range = _internal_range(
             lower, upper, lower_inc, upper_inc, False)
@@ -937,6 +940,11 @@ class DiscreteRange(Range):
     def __init__(self, *args, **kwargs):
         super(DiscreteRange, self).__init__(*args, **kwargs)
 
+        if self._range.empty:
+            # If the range has already been normalized,
+            # then we return here
+            return
+
         # Normalize the internal range
         lb = self._range.lower
         if not self.lower_inf and not self._range.lower_inc:


Test patch:
diff --git a/tests/test_range.py b/tests/test_range.py
index 3862057..8429884 100644
--- a/tests/test_range.py
+++ b/tests/test_range.py
@@ -1,4 +1,5 @@
 import pickle
+import itertools
 import pytest
 
 from spans import (
@@ -36,7 +37,7 @@ def test_bound_helper():
     assert _Bound(1, inc=False, is_lower=False) < _Bound(1, inc=True, is_lower=True)
     assert _Bound(1, inc=False, is_lower=False) < _Bound(1, inc=True, is_lower=False)
 
-    assert _Bound(1, inc=False, is_lower=True) < _Bound(1, inc=True, is_lower=False)
+    assert _Bound(1, inc=False, is_lower=True) > _Bound(1, inc=True, is_lower=False)
 
     assert _Bound(1, inc=True, is_lower=False) < _Bound(1, inc=False, is_lower=True)
 
@@ -453,6 +454,7 @@ def test_overlap(a, b):
     (floatrange(1.0, 5.0), floatrange(5.0, 10.0)),
     (floatrange(1.0, 5.0), floatrange(5.0, 10.0, lower_inc=False)),
     (floatrange(upper=5.0), floatrange(5.0)),
+    (floatrange(1.0, 5.0, upper_inc=True), floatrange(5.0, 10.0, lower_inc=False))
 ])
 def test_not_overlap(a, b):
     assert not a.overlap(b)
@@ -627,3 +629,17 @@ def test_bug11_valid_union_call_detected_as_invalid():
     b = floatrange(middle, end)
 
     assert a.union(b) == floatrange(start, end)
+
+@pytest.mark.parametrize("value, empty", [
+    (floatrange(5.0, 5.0, lower_inc=True, upper_inc=True), False),
+    (floatrange(5.0, 5.0, lower_inc=True, upper_inc=False), True),
+    (floatrange(5.0, 5.0, lower_inc=True, upper_inc=False), True), 
+    (floatrange(5.0, 5.0, lower_inc=False, upper_inc=True), True),
+    (floatrange(0.0, 5.0, upper_inc=True).intersection(
+        floatrange(5.0, lower_inc=False)), True)
+])
+def test_bug18_range_not_normalized_to_empty(value, empty):
+    """
+    `Bug #18 <https://github.com/runfalk/spans/issues/18>`_
+    """
+    assert bool(value) is not empty