Repository: pandas-dev/pandas

Issue:
BUG: RangeIndex.get_indexer is incorrect for some decreasing RangeIndex
For some decreasing `RangeIndex`, the `get_indexer` method will indicate that all of it's own values are missing, and find matches for values not included in the index:
```python
In [2]: ri = pd.RangeIndex(10, 0, -3)

In [3]: ri.get_indexer(ri)
Out[3]: array([-1, -1, -1, -1])

In [4]: ri.get_indexer(ri - 1)
Out[4]: array([ 1,  2,  3, -1])
```

This will in turn result in methods like `Series.reindex` not working properly:
```python
In [5]: s = pd.Series(list('abcd'), index=ri) 

In [6]: s
Out[6]: 
10    a
7     b
4     c
1     d
dtype: object

In [7]: s.reindex([10, 9, 7])
Out[7]: 
10    NaN
9       b
7     NaN
dtype: object
```

The issue appears to occur specifically for decreasing `RangeIndex` that are not in their canonical form.  By canonical form, I mean when `stop` is the next valid value in the range that's not included, e.g. when you think of a more standard range like `range(1, 7, 1)`, 7 is the next valid value that's not present, but when the step is larger than 1 you lose uniqueness of representation with  `stop` (i.e. `range(1, 6, 2) == range(1, 7, 2)`).

Note that the code above works properly for the equivalent `RangeIndex` in it's canonical form:
```python
In [8]: ri2 = pd.RangeIndex(start=10, stop=-2, step=-3)

In [9]: ri2.equals(ri)
Out[9]: True

In [10]: ri2.get_indexer(ri2)
Out[10]: array([0, 1, 2, 3])

In [11]: ri2.get_indexer(ri2 - 1)
Out[11]: array([-1, -1, -1, -1])

In [12]: s2 = pd.Series(list('abcd'), index=ri2)

In [13]: s2
Out[13]: 
10    a
7     b
4     c
1     d
dtype: object

In [14]: s2.reindex([10, 9, 7])
Out[14]: 
10      a
9     NaN
7       b
dtype: object
```

The cause of the issue appears to be that the code to determine `start`, `stop`, `step` when dealing with decreasing `RangeIndex` in `get_indexer` assumes `self.stop` is the canonical form:

https://github.com/pandas-dev/pandas/blob/c4489cbfa5f93e69301486e1ef867caed9f4eb9e/pandas/core/indexes/range.py#L386-L390

Instead of directly computing the reversed values ourselves, I think we should simply take the values from the reversed underlying `range` object:
```diff
diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py
index 8783351cc..4c5904e5a 100644
--- a/pandas/core/indexes/range.py
+++ b/pandas/core/indexes/range.py
@@ -387,7 +387,8 @@ class RangeIndex(Int64Index):
             start, stop, step = self.start, self.stop, self.step
         else:
             # Work on reversed range for simplicity:
-            start, stop, step = (self.stop - self.step, self.start + 1, -self.step)
+            reverse = self._range[::-1]
+            start, stop, step = reverse.start, reverse.stop, reverse.step
 
         target_array = np.asarray(target)
         if not (is_integer_dtype(target_array) and target_array.ndim == 1):
```


Discussion:

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
@@ -388,8 +388,9 @@ def get_indexer(self, target, method=None, limit=None, tolerance=None):
         if self.step > 0:
             start, stop, step = self.start, self.stop, self.step
         else:
-            # Work on reversed range for simplicity:
-            start, stop, step = (self.stop - self.step, self.start + 1, -self.step)
+            # GH 28678: work on reversed range for simplicity
+            reverse = self._range[::-1]
+            start, stop, step = reverse.start, reverse.stop, reverse.step
 
         target_array = np.asarray(target)
         if not (is_integer_dtype(target_array) and target_array.ndim == 1):


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
@@ -424,6 +424,14 @@ def test_get_indexer_limit(self):
         expected = np.array([0, 1, 2, 3, 3, -1], dtype=np.intp)
         tm.assert_numpy_array_equal(result, expected)
 
+    @pytest.mark.parametrize("stop", [0, -1, -2])
+    def test_get_indexer_decreasing(self, stop):
+        # GH 28678
+        index = RangeIndex(7, stop, -3)
+        result = index.get_indexer(range(9))
+        expected = np.array([-1, 2, -1, -1, 1, -1, -1, 0, -1], dtype=np.intp)
+        tm.assert_numpy_array_equal(result, expected)
+
     def test_join_outer(self):
         # join with Int64Index
         other = Int64Index(np.arange(25, 14, -1))