Repository: pandas-dev/pandas

Issue:
BUG: Intersection of RangeIndexes fails when the step sign is negative
#### Code Sample, a copy-pastable example if possible
Setup
```python
In [1]: import pandas as pd
   ...: idx1 = pd.RangeIndex(10)
   ...: idx2 = pd.RangeIndex(9, -1, -1)
   ...: idx3 = pd.RangeIndex(9, 5, -1)
   ...:
```

Issue 1: Mixed sign steps
```python
In [2]: idx1.intersection(idx2)
Out[2]: RangeIndex(start=0, stop=10, step=1)

In [3]: idx2.intersection(idx1)
Out[3]: RangeIndex(start=0, stop=10, step=-1)
```

Issue 2: Both steps negative
```python
In [4]: idx2.intersection(idx3)
Out[4]: RangeIndex(start=6, stop=10, step=-1)
```

#### Problem description
Issue 1: The output is inconsistent depending on the order of the intersection; one returns an index identical to `idx1`, the other an empty index.

Issue 2:  The returned index is empty, even though `idx3` is a subset of `idx2`.

#### Expected Output
Issue 1:  I'm not 100% sure what the output for Issue 1 should be.  To obey the properties of set intersections, all ten values should be returned, but what should the sign of the returned step be?

Seems like the options are:
- Always default to positive step?
- Default to the step sign of the first index in the intersection?
- Or does the step sign not matter as long as the values within the index are correct?

Issue 2:  An index identical to `idx3` should be returned.  Should the step sign be negative in this case, since both inputs have a negative step?  Or should we always default to positive?

Seems like this issue could be lumped in with #13432.  However, this issue seems restricted to just `RangeIndex` (all code modifications for a fix would be contained to range.py), whereas the other issue seems to be broader than a specific type of index.  Regardless, I'm happy to work on a PR to fix this specific bug.

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.2.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 78 Stepping 3, GenuineIntel
byteorder: little
LC_ALL: None
LANG: None
LOCALE: None.None

pandas: 0.20.3
pytest: 3.1.2
pip: 9.0.1
setuptools: 27.2.0
Cython: 0.26
numpy: 1.13.1
scipy: 0.19.1
xarray: None
IPython: 6.1.0
sphinx: 1.5.6
patsy: 0.4.1
dateutil: 2.6.0
pytz: 2017.2
blosc: None
bottleneck: 1.2.1
tables: 3.2.2
numexpr: 2.6.2
feather: None
matplotlib: 2.0.2
openpyxl: 2.4.7
xlrd: 1.0.0
xlwt: 1.2.0
xlsxwriter: 0.9.6
lxml: 3.7.3
bs4: 4.6.0
html5lib: 0.999
sqlalchemy: 1.1.9
pymysql: None
psycopg2: None
jinja2: 2.9.6
s3fs: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
Thanks for the report, this is distinct enough from #13432 to warrant a new issue

> Or does the step sign not matter as long as the values within the index are correct?

I think this is correct. `RangeIndex` is just a performance thing, I don't think we can make any guarantees beyond that the values will be correct (including whether or not you get back a RangeIndex or regular Index). All of our tests should be of the form

```
assert_frame_equal(idx1 & idx2, Index(list(idx1)) &Index list(idx2)))
```

with additional checks for whether or not the return value should be a RangeIndex.

Implementation wise, I'd say just do whatever is easiest. I suspect that will be

- positive step when both are positive
- negative step when both are negative
- not sure about mixed

Resolving patch:
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -324,12 +324,13 @@ def intersection(self, other):
         if not len(self) or not len(other):
             return RangeIndex._simple_new(None)
 
+        first = self[::-1] if self._step < 0 else self
+        second = other[::-1] if other._step < 0 else other
+
         # check whether intervals intersect
         # deals with in- and decreasing ranges
-        int_low = max(min(self._start, self._stop + 1),
-                      min(other._start, other._stop + 1))
-        int_high = min(max(self._stop, self._start + 1),
-                       max(other._stop, other._start + 1))
+        int_low = max(first._start, second._start)
+        int_high = min(first._stop, second._stop)
         if int_high <= int_low:
             return RangeIndex._simple_new(None)
 
@@ -337,21 +338,24 @@ def intersection(self, other):
         # solve intersection problem
         # performance hint: for identical step sizes, could use
         # cheaper alternative
-        gcd, s, t = self._extended_gcd(self._step, other._step)
+        gcd, s, t = first._extended_gcd(first._step, second._step)
 
         # check whether element sets intersect
-        if (self._start - other._start) % gcd:
+        if (first._start - second._start) % gcd:
             return RangeIndex._simple_new(None)
 
         # calculate parameters for the RangeIndex describing the
         # intersection disregarding the lower bounds
-        tmp_start = self._start + (other._start - self._start) * \
-            self._step // gcd * s
-        new_step = self._step * other._step // gcd
+        tmp_start = first._start + (second._start - first._start) * \
+            first._step // gcd * s
+        new_step = first._step * second._step // gcd
         new_index = RangeIndex(tmp_start, int_high, new_step, fastpath=True)
 
         # adjust index to limiting interval
         new_index._start = new_index._min_fitting_element(int_low)
+
+        if (self._step < 0 and other._step < 0) is not (new_index._step < 0):
+            new_index = new_index[::-1]
         return new_index
 
     def _min_fitting_element(self, lower_limit):


Test patch:
diff --git a/pandas/tests/indexes/test_range.py b/pandas/tests/indexes/test_range.py
--- a/pandas/tests/indexes/test_range.py
+++ b/pandas/tests/indexes/test_range.py
@@ -610,6 +610,21 @@ def test_intersection(self):
                                                 other.values)))
         tm.assert_index_equal(result, expected)
 
+        # reversed (GH 17296)
+        result = other.intersection(self.index)
+        tm.assert_index_equal(result, expected)
+
+        # GH 17296: intersect two decreasing RangeIndexes
+        first = RangeIndex(10, -2, -2)
+        other = RangeIndex(5, -4, -1)
+        expected = first.astype(int).intersection(other.astype(int))
+        result = first.intersection(other).astype(int)
+        tm.assert_index_equal(result, expected)
+
+        # reversed
+        result = other.intersection(first).astype(int)
+        tm.assert_index_equal(result, expected)
+
         index = RangeIndex(5)
 
         # intersect of non-overlapping indices