1from __future__ import annotations
2
3from collections.abc import (
4 Callable,
5 Hashable,
6 Iterator,
7 Sequence,
8)
9from datetime import timedelta
10import operator
11from sys import getsizeof
12from typing import (
13 TYPE_CHECKING,
14 Any,
15 Literal,
16 Self,
17 cast,
18 overload,
19)
20import warnings
21
22import numpy as np
23
24from pandas._libs import (
25 index as libindex,
26 lib,
27)
28from pandas._libs.internals import BlockValuesRefs
29from pandas._libs.lib import no_default
30from pandas.compat.numpy import function as nv
31from pandas.errors import Pandas4Warning
32from pandas.util._decorators import (
33 cache_readonly,
34 set_module,
35)
36from pandas.util._exceptions import find_stack_level
37
38from pandas.core.dtypes.base import ExtensionDtype
39from pandas.core.dtypes.cast import maybe_unbox_numpy_scalar
40from pandas.core.dtypes.common import (
41 ensure_platform_int,
42 ensure_python_int,
43 is_float,
44 is_integer,
45 is_scalar,
46 is_signed_integer_dtype,
47)
48from pandas.core.dtypes.generic import ABCTimedeltaIndex
49
50from pandas.core import ops
51import pandas.core.common as com
52from pandas.core.construction import extract_array
53from pandas.core.indexers import check_array_indexer
54import pandas.core.indexes.base as ibase
55from pandas.core.indexes.base import (
56 Index,
57 maybe_extract_name,
58)
59from pandas.core.ops.common import unpack_zerodim_and_defer
60
61if TYPE_CHECKING:
62 from pandas._typing import (
63 ArrayLike,
64 Axes,
65 Axis,
66 AxisInt,
67 Dtype,
68 JoinHow,
69 NaPosition,
70 NumpySorter,
71 NumpyValueArrayLike,
72 ScalarLike_co,
73 npt,
74 )
75
76 from pandas import Series
77 from pandas.core.arrays import ExtensionArray
78
79_empty_range = range(0)
80_dtype_int64 = np.dtype(np.int64)
81
82
83def min_fitting_element(start: int, step: int, lower_limit: int) -> int:
84 """Returns the smallest element greater than or equal to the limit"""
85 no_steps = -(-(lower_limit - start) // abs(step))
86 return start + abs(step) * no_steps
87
88
89@set_module("pandas")
90class RangeIndex(Index):
91 """
92 Immutable Index implementing a monotonic integer range.
93
94 RangeIndex is a memory-saving special case of an Index limited to representing
95 monotonic ranges with a 64-bit dtype. Using RangeIndex may in some instances
96 improve computing speed.
97
98 This is the default index type used
99 by DataFrame and Series when no explicit index is provided by the user.
100
101 Parameters
102 ----------
103 start : int, range, or other RangeIndex instance, default None
104 If int and "stop" is not given, interpreted as "stop" instead.
105 stop : int, default None
106 The end value of the range (exclusive).
107 step : int, default None
108 The step size of the range.
109 dtype : np.int64, default None
110 Unused, accepted for homogeneity with other index types.
111 copy : bool, default False
112 Unused, accepted for homogeneity with other index types.
113 name : object, optional
114 Name to be stored in the index.
115
116 Attributes
117 ----------
118 start
119 stop
120 step
121
122 Methods
123 -------
124 from_range
125
126 See Also
127 --------
128 Index : The base pandas Index type.
129
130 Examples
131 --------
132 >>> list(pd.RangeIndex(5))
133 [0, 1, 2, 3, 4]
134
135 >>> list(pd.RangeIndex(-2, 4))
136 [-2, -1, 0, 1, 2, 3]
137
138 >>> list(pd.RangeIndex(0, 10, 2))
139 [0, 2, 4, 6, 8]
140
141 >>> list(pd.RangeIndex(2, -10, -3))
142 [2, -1, -4, -7]
143
144 >>> list(pd.RangeIndex(0))
145 []
146
147 >>> list(pd.RangeIndex(1, 0))
148 []
149 """
150
151 _typ = "rangeindex"
152 _dtype_validation_metadata = (is_signed_integer_dtype, "signed integer")
153 _range: range
154 _values: np.ndarray
155
156 @property
157 def _engine_type(self) -> type[libindex.Int64Engine]:
158 return libindex.Int64Engine
159
160 # --------------------------------------------------------------------
161 # Constructors
162
163 def __new__(
164 cls,
165 start: int | np.integer | range | RangeIndex | None = None,
166 stop: int | np.integer | None = None,
167 step: int | np.integer | None = None,
168 dtype: Dtype | None = None,
169 copy: bool = False,
170 name: Hashable | None = None,
171 ) -> Self:
172 cls._validate_dtype(dtype)
173 name = maybe_extract_name(name, start, cls)
174
175 # RangeIndex
176 if isinstance(start, cls):
177 return start.copy(name=name)
178 elif isinstance(start, range):
179 return cls._simple_new(start, name=name)
180
181 # validate the arguments
182 if com.all_none(start, stop, step):
183 raise TypeError("RangeIndex(...) must be called with integers")
184
185 start = ensure_python_int(start) if start is not None else 0 # type: ignore[arg-type]
186
187 if stop is None:
188 start, stop = 0, start
189 else:
190 stop = ensure_python_int(stop)
191
192 step = ensure_python_int(step) if step is not None else 1
193 if step == 0:
194 raise ValueError("Step must not be zero")
195
196 rng = range(start, stop, step)
197 return cls._simple_new(rng, name=name)
198
199 @classmethod
200 def from_range(
201 cls, data: range, name: Hashable | None = None, dtype: Dtype | None = None
202 ) -> Self:
203 """
204 Create :class:`pandas.RangeIndex` from a ``range`` object.
205
206 This method provides a way to create a :class:`pandas.RangeIndex` directly
207 from a Python ``range`` object. The resulting :class:`RangeIndex` will have
208 the same start, stop, and step values as the input ``range`` object.
209 It is particularly useful for constructing indices in an efficient and
210 memory-friendly manner.
211
212 Parameters
213 ----------
214 data : range
215 The range object to be converted into a RangeIndex.
216 name : str, default None
217 Name to be stored in the index.
218 dtype : Dtype or None
219 Data type for the RangeIndex. If None, the default integer type will
220 be used.
221
222 Returns
223 -------
224 RangeIndex
225
226 See Also
227 --------
228 RangeIndex : Immutable Index implementing a monotonic integer range.
229 Index : Immutable sequence used for indexing and alignment.
230
231 Examples
232 --------
233 >>> pd.RangeIndex.from_range(range(5))
234 RangeIndex(start=0, stop=5, step=1)
235
236 >>> pd.RangeIndex.from_range(range(2, -10, -3))
237 RangeIndex(start=2, stop=-10, step=-3)
238 """
239 if not isinstance(data, range):
240 raise TypeError(
241 f"{cls.__name__}(...) must be called with object coercible to a "
242 f"range, {data!r} was passed"
243 )
244 cls._validate_dtype(dtype)
245 return cls._simple_new(data, name=name)
246
247 # error: Argument 1 of "_simple_new" is incompatible with supertype "Index";
248 # supertype defines the argument type as
249 # "Union[ExtensionArray, ndarray[Any, Any]]" [override]
250 @classmethod
251 def _simple_new( # type: ignore[override]
252 cls, values: range, name: Hashable | None = None
253 ) -> Self:
254 result = object.__new__(cls)
255
256 assert isinstance(values, range)
257
258 result._range = values
259 result._name = name
260 result._cache = {}
261 result._reset_identity()
262 # result._references populated lazily
263 return result
264
265 @cache_readonly
266 def _references(self) -> BlockValuesRefs: # type: ignore[override]
267 result = BlockValuesRefs()
268 result.add_index_reference(self)
269 return result
270
271 @classmethod
272 def _validate_dtype(cls, dtype: Dtype | None) -> None:
273 if dtype is None:
274 return
275
276 validation_func, expected = cls._dtype_validation_metadata
277 if not validation_func(dtype):
278 raise ValueError(
279 f"Incorrect `dtype` passed: expected {expected}, received {dtype}"
280 )
281
282 # --------------------------------------------------------------------
283
284 # error: Return type "Type[Index]" of "_constructor" incompatible with return
285 # type "Type[RangeIndex]" in supertype "Index"
286 @cache_readonly
287 def _constructor(self) -> type[Index]: # type: ignore[override]
288 """return the class to use for construction"""
289 return Index
290
291 # error: Signature of "_data" incompatible with supertype "Index"
292 @cache_readonly
293 def _data(self) -> np.ndarray: # type: ignore[override]
294 """
295 An int array that for performance reasons is created only when needed.
296
297 The constructed array is saved in ``_cache``.
298 """
299 return np.arange(self.start, self.stop, self.step, dtype=np.int64)
300
301 def _get_data_as_items(self) -> list[tuple[str, int]]:
302 """return a list of tuples of start, stop, step"""
303 rng = self._range
304 return [("start", rng.start), ("stop", rng.stop), ("step", rng.step)]
305
306 def __reduce__(self) -> tuple:
307 d = {"name": self._name}
308 d.update(dict(self._get_data_as_items()))
309 return ibase._new_Index, (type(self), d), None
310
311 # --------------------------------------------------------------------
312 # Rendering Methods
313
314 def _format_attrs(self) -> list[tuple[str, str | int]]: # type: ignore[override]
315 """
316 Return a list of tuples of the (attr, formatted_value)
317 """
318 attrs = cast("list[tuple[str, str | int]]", self._get_data_as_items())
319 if self._name is not None:
320 attrs.append(("name", ibase.default_pprint(self._name)))
321 return attrs
322
323 def _format_with_header(self, *, header: list[str], na_rep: str) -> list[str]:
324 # Equivalent to Index implementation, but faster
325 if not len(self._range):
326 return header
327 first_val_str = str(self._range[0])
328 last_val_str = str(self._range[-1])
329 max_length = max(len(first_val_str), len(last_val_str))
330
331 return header + [f"{x:<{max_length}}" for x in self._range]
332
333 # --------------------------------------------------------------------
334
335 @property
336 def start(self) -> int:
337 """
338 The value of the `start` parameter (``0`` if this was not supplied).
339
340 This property returns the starting value of the `RangeIndex`. If the `start`
341 value is not explicitly provided during the creation of the `RangeIndex`,
342 it defaults to 0.
343
344 See Also
345 --------
346 RangeIndex : Immutable index implementing a range-based index.
347 RangeIndex.stop : Returns the stop value of the `RangeIndex`.
348 RangeIndex.step : Returns the step value of the `RangeIndex`.
349
350 Examples
351 --------
352 >>> idx = pd.RangeIndex(5)
353 >>> idx.start
354 0
355
356 >>> idx = pd.RangeIndex(2, -10, -3)
357 >>> idx.start
358 2
359 """
360 # GH 25710
361 return self._range.start
362
363 @property
364 def stop(self) -> int:
365 """
366 The value of the `stop` parameter.
367
368 This property returns the `stop` value of the RangeIndex, which defines the
369 upper (or lower, in case of negative steps) bound of the index range. The
370 `stop` value is exclusive, meaning the RangeIndex includes values up to but
371 not including this value.
372
373 See Also
374 --------
375 RangeIndex : Immutable index representing a range of integers.
376 RangeIndex.start : The start value of the RangeIndex.
377 RangeIndex.step : The step size between elements in the RangeIndex.
378
379 Examples
380 --------
381 >>> idx = pd.RangeIndex(5)
382 >>> idx.stop
383 5
384
385 >>> idx = pd.RangeIndex(2, -10, -3)
386 >>> idx.stop
387 -10
388 """
389 return self._range.stop
390
391 @property
392 def step(self) -> int:
393 """
394 The value of the `step` parameter (``1`` if this was not supplied).
395
396 The ``step`` parameter determines the increment (or decrement in the case
397 of negative values) between consecutive elements in the ``RangeIndex``.
398
399 See Also
400 --------
401 RangeIndex : Immutable index implementing a range-based index.
402 RangeIndex.stop : Returns the stop value of the RangeIndex.
403 RangeIndex.start : Returns the start value of the RangeIndex.
404
405 Examples
406 --------
407 >>> idx = pd.RangeIndex(5)
408 >>> idx.step
409 1
410
411 >>> idx = pd.RangeIndex(2, -10, -3)
412 >>> idx.step
413 -3
414
415 Even if :class:`pandas.RangeIndex` is empty, ``step`` is still ``1`` if
416 not supplied.
417
418 >>> idx = pd.RangeIndex(1, 0)
419 >>> idx.step
420 1
421 """
422 # GH 25710
423 return self._range.step
424
425 @cache_readonly
426 def nbytes(self) -> int:
427 """
428 Return the number of bytes in the underlying data.
429 """
430 rng = self._range
431 # passing a default to getsizeof avoids a TypeError on PyPy, where
432 # sys.getsizeof always raises TypeError unless a default is provided
433 # (GH#46176)
434 objsize = 24
435 return getsizeof(rng, objsize) + sum(
436 getsizeof(getattr(rng, attr_name), objsize)
437 for attr_name in ["start", "stop", "step"]
438 )
439
440 def memory_usage(self, deep: bool = False) -> int:
441 """
442 Memory usage of my values
443
444 Parameters
445 ----------
446 deep : bool
447 Introspect the data deeply, interrogate
448 `object` dtypes for system-level memory consumption
449
450 Returns
451 -------
452 bytes used
453
454 Notes
455 -----
456 Memory usage does not include memory consumed by elements that
457 are not components of the array if deep=False
458
459 See Also
460 --------
461 numpy.ndarray.nbytes
462 """
463 return self.nbytes
464
465 @property
466 def dtype(self) -> np.dtype:
467 return _dtype_int64
468
469 @property
470 def is_unique(self) -> bool:
471 """return if the index has unique values"""
472 return True
473
474 @cache_readonly
475 def is_monotonic_increasing(self) -> bool:
476 return self._range.step > 0 or len(self) <= 1
477
478 @cache_readonly
479 def is_monotonic_decreasing(self) -> bool:
480 return self._range.step < 0 or len(self) <= 1
481
482 def __contains__(self, key: Any) -> bool:
483 hash(key)
484 try:
485 key = ensure_python_int(key)
486 except (TypeError, OverflowError):
487 return False
488 return key in self._range
489
490 @property
491 def inferred_type(self) -> str:
492 return "integer"
493
494 # --------------------------------------------------------------------
495 # Indexing Methods
496
497 def get_loc(self, key: int) -> int: # type: ignore[override]
498 """
499 Get integer location for requested label.
500
501 Parameters
502 ----------
503 key : int or float
504 Label to locate. Integer-like floats (e.g. 3.0) are accepted and
505 treated as the corresponding integer. Non-integer floats and other
506 non-integer labels are not valid and will raise KeyError or
507 InvalidIndexError.
508
509 Returns
510 -------
511 int
512 Integer location of the label within the RangeIndex.
513
514 Raises
515 ------
516 KeyError
517 If the label is not present in the RangeIndex or the label is a
518 non-integer value.
519 InvalidIndexError
520 If the label is of an invalid type for the RangeIndex.
521
522 See Also
523 --------
524 RangeIndex.get_slice_bound : Calculate slice bound that corresponds to
525 given label.
526 RangeIndex.get_indexer : Computes indexer and mask for new index given
527 the current index.
528 RangeIndex.get_non_unique : Returns indexer and masks for new index given
529 the current index.
530 RangeIndex.get_indexer_for : Returns an indexer even when non-unique.
531
532 Examples
533 --------
534 >>> idx = pd.RangeIndex(5)
535 >>> idx.get_loc(3)
536 3
537
538 >>> idx = pd.RangeIndex(2, 10, 2) # values [2, 4, 6, 8]
539 >>> idx.get_loc(6)
540 2
541 """
542 if is_integer(key) or (is_float(key) and key.is_integer()):
543 new_key = int(key)
544 try:
545 return self._range.index(new_key)
546 except ValueError as err:
547 raise KeyError(key) from err
548 if isinstance(key, Hashable):
549 raise KeyError(key)
550 self._check_indexing_error(key)
551 raise KeyError(key)
552
553 def _get_indexer(
554 self,
555 target: Index,
556 method: str | None = None,
557 limit: int | None = None,
558 tolerance: Any = None,
559 ) -> npt.NDArray[np.intp]:
560 if com.any_not_none(method, tolerance, limit):
561 return super()._get_indexer(
562 target, method=method, tolerance=tolerance, limit=limit
563 )
564
565 if self.step > 0:
566 start, stop, step = self.start, self.stop, self.step
567 else:
568 # GH 28678: work on reversed range for simplicity
569 reverse = self._range[::-1]
570 start, stop, step = reverse.start, reverse.stop, reverse.step
571
572 target_array = np.asarray(target)
573 if target_array.dtype.kind in "iu":
574 # GH#64148: ``target_array - start`` overflows int64 for extreme
575 # targets and for ranges spanning more than INT64_MAX. Establish
576 # membership with exact comparisons first; an in-window offset then
577 # fits in uint64, so the modular arithmetic below is exact.
578 valid = (target_array >= start) & (target_array < stop)
579 start_uint = np.uint64(start % 2**64)
580 step_uint = np.uint64(step)
581 locs_uint = target_array.astype(np.uint64, copy=False) - start_uint
582 valid &= locs_uint % step_uint == 0
583 locs = np.where(valid, (locs_uint // step_uint).astype(np.int64), -1)
584 if step != self.step:
585 # We reversed this range: transform to original locs
586 locs = np.where(valid, len(self) - 1 - locs, -1)
587 return ensure_platform_int(locs)
588
589 locs = target_array - start
590 valid = (locs % step == 0) & (locs >= 0) & (target_array < stop)
591 locs[~valid] = -1
592 locs[valid] = locs[valid] / step
593
594 if step != self.step:
595 # We reversed this range: transform to original locs
596 locs[valid] = len(self) - 1 - locs[valid]
597 return ensure_platform_int(locs)
598
599 @cache_readonly
600 def _should_fallback_to_positional(self) -> bool:
601 """
602 Should an integer key be treated as positional?
603 """
604 return False
605
606 # --------------------------------------------------------------------
607
608 def tolist(self) -> list[int]:
609 return list(self._range)
610
611 def __iter__(self) -> Iterator[int]:
612 """
613 Return an iterator of the values.
614
615 Returns
616 -------
617 iterator
618 An iterator yielding ints from the RangeIndex.
619
620 Examples
621 --------
622 >>> idx = pd.RangeIndex(3)
623 >>> for x in idx:
624 ... print(x)
625 0
626 1
627 2
628 """
629 yield from self._range
630
631 def _shallow_copy( # type: ignore[override]
632 self, values: ArrayLike, name: Hashable = no_default
633 ) -> Index:
634 """
635 Create a new RangeIndex with the same class as the caller, don't copy the
636 data, use the same object attributes with passed in attributes taking
637 precedence.
638
639 *this is an internal non-public method*
640
641 Parameters
642 ----------
643 values : the values to create the new RangeIndex, optional
644 name : Label, defaults to self.name
645 """
646 name = self._name if name is no_default else name
647
648 if values.dtype.kind == "f":
649 return Index(values, name=name, dtype=np.float64, copy=False)
650 if values.dtype.kind == "i" and values.ndim == 1:
651 # GH 46675 & 43885: If values is equally spaced, return a
652 # more memory-compact RangeIndex instead of Index with 64-bit dtype
653 if len(values) == 1:
654 start = values[0]
655 new_range = range(start, start + self.step, self.step)
656 return type(self)._simple_new(new_range, name=name)
657 maybe_range = ibase.maybe_sequence_to_range(values)
658 if isinstance(maybe_range, range):
659 return type(self)._simple_new(maybe_range, name=name)
660 return self._constructor._simple_new(values, name=name)
661
662 def _view(self) -> Self:
663 result = type(self)._simple_new(self._range, name=self._name)
664 result._cache = self._cache
665 self._references.add_index_reference(result)
666 return result
667
668 def _wrap_reindex_result(
669 self, target: Index, indexer: npt.NDArray[np.intp] | None, preserve_names: bool
670 ) -> Index:
671 if not isinstance(target, type(self)) and target.dtype.kind == "i":
672 target = self._shallow_copy(target._values, name=target.name)
673 return super()._wrap_reindex_result(target, indexer, preserve_names)
674
675 def copy(self, name: Hashable | None = None, deep: bool = False) -> Self:
676 """
677 Make a copy of this object.
678
679 Name is set on the new object.
680
681 Parameters
682 ----------
683 name : Label, optional
684 Set name for new object.
685 deep : bool, default False
686 If True attempts to make a deep copy of the RangeIndex.
687 Else makes a shallow copy.
688
689 Returns
690 -------
691 RangeIndex
692 RangeIndex refer to new object which is a copy of this object.
693
694 See Also
695 --------
696 RangeIndex.delete: Make new RangeIndex with passed location(-s) deleted.
697 RangeIndex.drop: Make new RangeIndex with passed list of labels deleted.
698
699 Notes
700 -----
701 In most cases, there should be no functional difference from using
702 ``deep``, but if ``deep`` is passed it will attempt to deepcopy.
703
704 Examples
705 --------
706 >>> idx = pd.RangeIndex(3)
707 >>> new_idx = idx.copy()
708 >>> idx is new_idx
709 False
710 """
711 name = self._validate_names(name=name, deep=deep)[0]
712 new_index = self._rename(name=name)
713 return new_index
714
715 def _minmax(self, meth: Literal["min", "max"]) -> int | float:
716 no_steps = len(self) - 1
717 if no_steps == -1:
718 return np.nan
719 elif (meth == "min" and self.step > 0) or (meth == "max" and self.step < 0):
720 return self.start
721
722 return self.start + self.step * no_steps
723
724 def min(
725 self,
726 axis: AxisInt | None = None,
727 skipna: bool = True,
728 *args: Any,
729 **kwargs: Any,
730 ) -> int | float:
731 """The minimum value of the RangeIndex"""
732 nv.validate_minmax_axis(axis)
733 nv.validate_min(args, kwargs)
734 return self._minmax("min")
735
736 def max(
737 self,
738 axis: AxisInt | None = None,
739 skipna: bool = True,
740 *args: Any,
741 **kwargs: Any,
742 ) -> int | float:
743 """The maximum value of the RangeIndex"""
744 nv.validate_minmax_axis(axis)
745 nv.validate_max(args, kwargs)
746 return self._minmax("max")
747
748 def _argminmax(
749 self,
750 meth: Literal["min", "max"],
751 axis: AxisInt | None = None,
752 skipna: bool = True,
753 ) -> int:
754 nv.validate_minmax_axis(axis)
755 if len(self) == 0:
756 return getattr(super(), f"arg{meth}")(
757 axis=axis,
758 skipna=skipna,
759 )
760 elif meth == "min":
761 if self.step > 0:
762 return 0
763 else:
764 return len(self) - 1
765 elif meth == "max":
766 if self.step > 0:
767 return len(self) - 1
768 else:
769 return 0
770 else:
771 raise ValueError(f"{meth=} must be max or min")
772
773 def argmin(
774 self,
775 axis: AxisInt | None = None,
776 skipna: bool = True,
777 *args: Any,
778 **kwargs: Any,
779 ) -> int:
780 nv.validate_argmin(args, kwargs)
781 return self._argminmax("min", axis=axis, skipna=skipna)
782
783 def argmax(
784 self,
785 axis: AxisInt | None = None,
786 skipna: bool = True,
787 *args: Any,
788 **kwargs: Any,
789 ) -> int:
790 nv.validate_argmax(args, kwargs)
791 return self._argminmax("max", axis=axis, skipna=skipna)
792
793 def argsort(self, *args: Any, **kwargs: Any) -> npt.NDArray[np.intp]:
794 """
795 Returns the indices that would sort the index and its
796 underlying data.
797
798 Returns
799 -------
800 np.ndarray[np.intp]
801
802 See Also
803 --------
804 numpy.ndarray.argsort
805 """
806 ascending = kwargs.pop("ascending", True) # EA compat
807 kwargs.pop("kind", None) # e.g. "mergesort" is irrelevant
808 nv.validate_argsort(args, kwargs)
809
810 start, stop, step = None, None, None
811 if self._range.step > 0:
812 if ascending:
813 start = len(self)
814 else:
815 start, stop, step = len(self) - 1, -1, -1
816 elif ascending:
817 start, stop, step = len(self) - 1, -1, -1
818 else:
819 start = len(self)
820
821 return np.arange(start, stop, step, dtype=np.intp)
822
823 def factorize(
824 self,
825 sort: bool = False,
826 use_na_sentinel: bool = True,
827 ) -> tuple[npt.NDArray[np.intp], RangeIndex]:
828 if sort and self.step < 0:
829 codes = np.arange(len(self) - 1, -1, -1, dtype=np.intp)
830 uniques = self[::-1]
831 else:
832 codes = np.arange(len(self), dtype=np.intp)
833 uniques = self
834 return codes, uniques
835
836 def equals(self, other: object) -> bool:
837 """
838 Determines if two Index objects contain the same elements.
839 """
840 if isinstance(other, RangeIndex):
841 return self._range == other._range
842 return super().equals(other)
843
844 @overload
845 def sort_values(
846 self,
847 *,
848 return_indexer: Literal[False] = ...,
849 ascending: bool = ...,
850 na_position: NaPosition = ...,
851 key: Callable | None = ...,
852 ) -> Self: ...
853
854 @overload
855 def sort_values(
856 self,
857 *,
858 return_indexer: Literal[True],
859 ascending: bool = ...,
860 na_position: NaPosition = ...,
861 key: Callable | None = ...,
862 ) -> tuple[Self, np.ndarray]: ...
863
864 @overload
865 def sort_values(
866 self,
867 *,
868 return_indexer: bool = ...,
869 ascending: bool = ...,
870 na_position: NaPosition = ...,
871 key: Callable | None = ...,
872 ) -> Self | tuple[Self, np.ndarray]: ...
873
874 def sort_values(
875 self,
876 *,
877 return_indexer: bool = False,
878 ascending: bool = True,
879 na_position: NaPosition = "last",
880 key: Callable | None = None,
881 ) -> Self | tuple[Self, np.ndarray]:
882 if key is not None:
883 return super().sort_values(
884 return_indexer=return_indexer,
885 ascending=ascending,
886 na_position=na_position,
887 key=key,
888 )
889 else:
890 sorted_index = self
891 inverse_indexer = False
892 if ascending:
893 if self.step < 0:
894 sorted_index = self[::-1]
895 inverse_indexer = True
896 elif self.step > 0:
897 sorted_index = self[::-1]
898 inverse_indexer = True
899
900 if return_indexer:
901 if inverse_indexer:
902 indexer = np.arange(len(self) - 1, -1, -1, dtype=np.intp)
903 else:
904 indexer = np.arange(len(self), dtype=np.intp)
905 return sorted_index, indexer
906 else:
907 return sorted_index
908
909 # --------------------------------------------------------------------
910 # Set Operations
911
912 def _intersection(self, other: Index, sort: bool = False) -> Index:
913 # caller is responsible for checking self and other are both non-empty
914
915 if not isinstance(other, RangeIndex):
916 return super()._intersection(other, sort=sort) # type: ignore[return-value]
917
918 first = self._range[::-1] if self.step < 0 else self._range
919 second = other._range[::-1] if other.step < 0 else other._range
920
921 # check whether intervals intersect
922 # deals with in- and decreasing ranges
923 int_low = max(first.start, second.start)
924 int_high = min(first.stop, second.stop)
925 if int_high <= int_low:
926 return self._simple_new(_empty_range)
927
928 # Method hint: linear Diophantine equation
929 # solve intersection problem
930 # performance hint: for identical step sizes, could use
931 # cheaper alternative
932 gcd, s, _ = self._extended_gcd(first.step, second.step)
933
934 # check whether element sets intersect
935 if (first.start - second.start) % gcd:
936 return self._simple_new(_empty_range)
937
938 # calculate parameters for the RangeIndex describing the
939 # intersection disregarding the lower bounds
940 tmp_start = first.start + (second.start - first.start) * first.step // gcd * s
941 new_step = first.step * second.step // gcd
942
943 # adjust index to limiting interval
944 new_start = min_fitting_element(tmp_start, new_step, int_low)
945 new_range = range(new_start, int_high, new_step)
946
947 if (self.step < 0 and other.step < 0) is not (new_range.step < 0):
948 new_range = new_range[::-1]
949
950 return self._simple_new(new_range)
951
952 def _extended_gcd(self, a: int, b: int) -> tuple[int, int, int]:
953 """
954 Extended Euclidean algorithms to solve Bezout's identity:
955 a*x + b*y = gcd(x, y)
956 Finds one particular solution for x, y: s, t
957 Returns: gcd, s, t
958 """
959 s, old_s = 0, 1
960 t, old_t = 1, 0
961 r, old_r = b, a
962 while r:
963 quotient = old_r // r
964 old_r, r = r, old_r - quotient * r
965 old_s, s = s, old_s - quotient * s
966 old_t, t = t, old_t - quotient * t
967 return old_r, old_s, old_t
968
969 def _range_in_self(self, other: range) -> bool:
970 """Check if other range is contained in self"""
971 # https://stackoverflow.com/a/32481015
972 if not other:
973 return True
974 if not self._range:
975 return False
976 if len(other) > 1 and other.step % self._range.step:
977 return False
978 return other.start in self._range and other[-1] in self._range
979
980 def _union(self, other: Index, sort: bool | None) -> Index:
981 """
982 Form the union of two Index objects and sorts if possible
983
984 Parameters
985 ----------
986 other : Index or array-like
987
988 sort : bool or None, default None
989 Whether to sort (monotonically increasing) the resulting index.
990 ``sort=None|True`` returns a ``RangeIndex`` if possible or a sorted
991 ``Index`` with an int64 dtype if not.
992 ``sort=False`` can return a ``RangeIndex`` if self is monotonically
993 increasing and other is fully contained in self. Otherwise, returns
994 an unsorted ``Index`` with an int64 dtype.
995
996 Returns
997 -------
998 union : Index
999 """
1000 if isinstance(other, RangeIndex):
1001 if sort in (None, True) or (
1002 sort is False and self.step > 0 and self._range_in_self(other._range)
1003 ):
1004 # GH 47557: Can still return a RangeIndex
1005 # if other range in self and sort=False
1006 start_s, step_s = self.start, self.step
1007 end_s = self.start + self.step * (len(self) - 1)
1008 start_o, step_o = other.start, other.step
1009 end_o = other.start + other.step * (len(other) - 1)
1010 if self.step < 0:
1011 start_s, step_s, end_s = end_s, -step_s, start_s
1012 if other.step < 0:
1013 start_o, step_o, end_o = end_o, -step_o, start_o
1014 if len(self) == 1 and len(other) == 1:
1015 step_s = step_o = abs(self.start - other.start)
1016 elif len(self) == 1:
1017 step_s = step_o
1018 elif len(other) == 1:
1019 step_o = step_s
1020 start_r = min(start_s, start_o)
1021 end_r = max(end_s, end_o)
1022 if step_o == step_s:
1023 if (
1024 (start_s - start_o) % step_s == 0
1025 and (start_s - end_o) <= step_s
1026 and (start_o - end_s) <= step_s
1027 ):
1028 return type(self)(start_r, end_r + step_s, step_s)
1029 if (
1030 (step_s % 2 == 0)
1031 and (abs(start_s - start_o) == step_s / 2)
1032 and (abs(end_s - end_o) == step_s / 2)
1033 ):
1034 # e.g. range(0, 10, 2) and range(1, 11, 2)
1035 # but not range(0, 20, 4) and range(1, 21, 4) GH#44019
1036 return type(self)(start_r, end_r + step_s / 2, step_s / 2) # type: ignore[arg-type]
1037
1038 elif step_o % step_s == 0:
1039 if (
1040 (start_o - start_s) % step_s == 0
1041 and (start_o + step_s >= start_s)
1042 and (end_o - step_s <= end_s)
1043 ):
1044 return type(self)(start_r, end_r + step_s, step_s)
1045 elif step_s % step_o == 0:
1046 if (
1047 (start_s - start_o) % step_o == 0
1048 and (start_s + step_o >= start_o)
1049 and (end_s - step_o <= end_o)
1050 ):
1051 return type(self)(start_r, end_r + step_o, step_o)
1052
1053 return super()._union(other, sort=sort) # type: ignore[return-value]
1054
1055 def _difference(self, other: Index, sort: bool | None = None) -> Index:
1056 # optimized set operation if we have another RangeIndex
1057 self._validate_sort_keyword(sort)
1058 self._assert_can_do_setop(other)
1059 other, result_name = self._convert_can_do_setop(other)
1060
1061 if not isinstance(other, RangeIndex):
1062 return super()._difference(other, sort=sort)
1063
1064 if sort is not False and self.step < 0:
1065 return self[::-1]._difference(other)
1066
1067 res_name = ops.get_op_result_name(self, other) # type: ignore[no-untyped-call]
1068
1069 first = self._range[::-1] if self.step < 0 else self._range
1070 overlap = self.intersection(other)
1071 if overlap.step < 0: # type: ignore[attr-defined]
1072 overlap = overlap[::-1]
1073
1074 if len(overlap) == 0:
1075 return self.rename(name=res_name)
1076 if len(overlap) == len(self):
1077 return self[:0].rename(res_name)
1078
1079 # overlap.step will always be a multiple of self.step (see _intersection)
1080
1081 if len(overlap) == 1:
1082 if overlap[0] == self[0]:
1083 return self[1:]
1084
1085 elif overlap[0] == self[-1]:
1086 return self[:-1]
1087
1088 elif len(self) == 3 and overlap[0] == self[1]:
1089 return self[::2]
1090
1091 else:
1092 return super()._difference(other, sort=sort)
1093
1094 elif len(overlap) == 2 and overlap[0] == first[0] and overlap[-1] == first[-1]:
1095 # e.g. range(-8, 20, 7) and range(13, -9, -3)
1096 return self[1:-1]
1097
1098 if overlap.step == first.step: # type: ignore[attr-defined]
1099 if overlap[0] == first.start:
1100 # The difference is everything after the intersection
1101 new_rng = range(overlap[-1] + first.step, first.stop, first.step)
1102 elif overlap[-1] == first[-1]:
1103 # The difference is everything before the intersection
1104 new_rng = range(first.start, overlap[0], first.step)
1105 elif overlap._range == first[1:-1]: # type: ignore[attr-defined]
1106 # e.g. range(4) and range(1, 3)
1107 step = len(first) - 1
1108 new_rng = first[::step]
1109 else:
1110 # The difference is not range-like
1111 # e.g. range(1, 10, 1) and range(3, 7, 1)
1112 return super()._difference(other, sort=sort)
1113
1114 else:
1115 # We must have len(self) > 1, bc we ruled out above
1116 # len(overlap) == 0 and len(overlap) == len(self)
1117 assert len(self) > 1
1118
1119 if overlap.step == first.step * 2: # type: ignore[attr-defined]
1120 if overlap[0] == first[0] and overlap[-1] in (first[-1], first[-2]):
1121 # e.g. range(1, 10, 1) and range(1, 10, 2)
1122 new_rng = first[1::2]
1123
1124 elif overlap[0] == first[1] and overlap[-1] in (first[-1], first[-2]):
1125 # e.g. range(1, 10, 1) and range(2, 10, 2)
1126 new_rng = first[::2]
1127
1128 else:
1129 # We can get here with e.g. range(20) and range(0, 10, 2)
1130 return super()._difference(other, sort=sort)
1131
1132 else:
1133 # e.g. range(10) and range(0, 10, 3)
1134 return super()._difference(other, sort=sort)
1135
1136 if first is not self._range:
1137 new_rng = new_rng[::-1]
1138 new_index = type(self)._simple_new(new_rng, name=res_name)
1139
1140 return new_index
1141
1142 def symmetric_difference(
1143 self, other: Axes, result_name: Hashable | None = None, sort: bool | None = None
1144 ) -> Index:
1145 if not isinstance(other, RangeIndex) or sort is not None:
1146 return super().symmetric_difference(other, result_name, sort)
1147
1148 left = self.difference(other)
1149 right = other.difference(self)
1150 result = left.union(right)
1151
1152 if isinstance(result, RangeIndex) and result.step < 0:
1153 result = result[::-1]
1154
1155 if result_name is not None:
1156 result = result.rename(result_name)
1157 return result
1158
1159 def _join_empty(
1160 self, other: Index, how: JoinHow, sort: bool
1161 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
1162 if not isinstance(other, RangeIndex) and other.dtype.kind == "i":
1163 other = self._shallow_copy(other._values, name=other.name)
1164 return super()._join_empty(other, how=how, sort=sort)
1165
1166 def _join_monotonic(
1167 self, other: Index, how: JoinHow = "left"
1168 ) -> tuple[Index, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
1169 # This currently only gets called for the monotonic increasing case
1170 if not isinstance(other, type(self)):
1171 maybe_ri = self._shallow_copy(other._values, name=other.name)
1172 if not isinstance(maybe_ri, type(self)):
1173 # Cannot convert other to RangeIndex; fall back to
1174 # _join_via_get_indexer since RangeIndex._can_use_libjoin
1175 # is False.
1176 raise NotImplementedError
1177 other = maybe_ri
1178
1179 if self.equals(other):
1180 ret_index = other if how == "right" else self
1181 return ret_index, None, None
1182
1183 if how == "left":
1184 join_index = self
1185 lidx = None
1186 ridx = other.get_indexer(join_index)
1187 elif how == "right":
1188 join_index = other
1189 lidx = self.get_indexer(join_index)
1190 ridx = None
1191 elif how == "inner":
1192 join_index = self.intersection(other) # type: ignore[assignment]
1193 lidx = self.get_indexer(join_index)
1194 ridx = other.get_indexer(join_index)
1195 elif how == "outer":
1196 join_index = self.union(other) # type: ignore[assignment]
1197 lidx = self.get_indexer(join_index)
1198 ridx = other.get_indexer(join_index)
1199
1200 lidx = None if lidx is None else ensure_platform_int(lidx)
1201 ridx = None if ridx is None else ensure_platform_int(ridx)
1202 return join_index, lidx, ridx
1203
1204 # --------------------------------------------------------------------
1205
1206 # error: Return type "Index" of "delete" incompatible with return type
1207 # "RangeIndex" in supertype "Index"
1208 def delete( # type: ignore[override]
1209 self, loc: int | np.integer | list[int] | npt.NDArray[np.integer]
1210 ) -> Index:
1211 # In some cases we can retain RangeIndex, see also
1212 # DatetimeTimedeltaMixin._get_delete_Freq
1213 if is_integer(loc):
1214 if loc in (0, -len(self)):
1215 return self[1:]
1216 if loc in (-1, len(self) - 1):
1217 return self[:-1]
1218 if len(self) == 3 and loc in (1, -2):
1219 return self[::2]
1220
1221 elif lib.is_list_like(loc):
1222 slc = lib.maybe_indices_to_slice(np.asarray(loc, dtype=np.intp), len(self))
1223
1224 if isinstance(slc, slice):
1225 # defer to RangeIndex._difference, which is optimized to return
1226 # a RangeIndex whenever possible
1227 other = self[slc]
1228 return self.difference(other, sort=False)
1229
1230 return super().delete(loc)
1231
1232 def insert(self, loc: int, item: Hashable) -> Index:
1233 if is_integer(item) or is_float(item):
1234 # We can retain RangeIndex is inserting at the beginning or end,
1235 # or right in the middle.
1236 if len(self) == 0 and loc == 0 and is_integer(item):
1237 new_rng = range(item, item + self.step, self.step)
1238 return type(self)._simple_new(new_rng, name=self._name)
1239 elif len(self):
1240 rng = self._range
1241 if loc == 0 and item == self[0] - self.step:
1242 new_rng = range(rng.start - rng.step, rng.stop, rng.step)
1243 return type(self)._simple_new(new_rng, name=self._name)
1244
1245 elif loc == len(self) and item == self[-1] + self.step:
1246 new_rng = range(rng.start, rng.stop + rng.step, rng.step)
1247 return type(self)._simple_new(new_rng, name=self._name)
1248
1249 elif len(self) == 2 and item == self[0] + self.step / 2:
1250 # e.g. inserting 1 into [0, 2]
1251 step = int(self.step / 2)
1252 new_rng = range(self.start, self.stop, step)
1253 return type(self)._simple_new(new_rng, name=self._name)
1254
1255 return super().insert(loc, item)
1256
1257 def _concat(self, indexes: list[Index], name: Hashable) -> Index:
1258 """
1259 Overriding parent method for the case of all RangeIndex instances.
1260
1261 When all members of "indexes" are of type RangeIndex: result will be
1262 RangeIndex if possible, Index with an int64 dtype otherwise. E.g.:
1263 indexes = [RangeIndex(3), RangeIndex(3, 6)] -> RangeIndex(6)
1264 indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Index([0,1,2,4,5], dtype='int64')
1265 """
1266 if not all(isinstance(x, RangeIndex) for x in indexes):
1267 result = super()._concat(indexes, name)
1268 if result.dtype.kind == "i":
1269 return self._shallow_copy(result._values)
1270 return result
1271
1272 elif len(indexes) == 1:
1273 return indexes[0]
1274
1275 rng_indexes = cast("list[RangeIndex]", indexes)
1276
1277 start = step = next_ = None
1278
1279 # Filter the empty indexes
1280 non_empty_indexes = []
1281 all_same_index = True
1282 prev: RangeIndex | None = None
1283 for obj in rng_indexes:
1284 if len(obj):
1285 non_empty_indexes.append(obj)
1286 if all_same_index:
1287 if prev is not None:
1288 all_same_index = prev.equals(obj)
1289 else:
1290 prev = obj
1291
1292 for obj in non_empty_indexes:
1293 rng = obj._range
1294
1295 if start is None:
1296 # This is set by the first non-empty index
1297 start = rng.start
1298 if step is None and len(rng) > 1:
1299 step = rng.step
1300 elif step is None:
1301 # First non-empty index had only one element
1302 if rng.start == start:
1303 if all_same_index:
1304 values = np.tile(
1305 non_empty_indexes[0]._values, len(non_empty_indexes)
1306 )
1307 else:
1308 values = np.concatenate([x._values for x in rng_indexes])
1309 result = self._constructor(values, copy=False)
1310 return result.rename(name)
1311
1312 step = rng.start - start
1313
1314 non_consecutive = (step != rng.step and len(rng) > 1) or (
1315 next_ is not None and rng.start != next_
1316 )
1317 if non_consecutive:
1318 if all_same_index:
1319 values = np.tile(
1320 non_empty_indexes[0]._values, len(non_empty_indexes)
1321 )
1322 else:
1323 values = np.concatenate([x._values for x in rng_indexes])
1324 result = self._constructor(values, copy=False)
1325 return result.rename(name)
1326
1327 if step is not None:
1328 next_ = rng[-1] + step
1329
1330 if non_empty_indexes:
1331 # Get the stop value from "next" or alternatively
1332 # from the last non-empty index
1333 stop = non_empty_indexes[-1].stop if next_ is None else next_
1334 if len(non_empty_indexes) == 1:
1335 step = non_empty_indexes[0].step
1336 return RangeIndex(start, stop, step, name=name)
1337
1338 # Here all "indexes" had 0 length, i.e. were empty.
1339 # In this case return an empty range index.
1340 return RangeIndex(_empty_range, name=name)
1341
1342 def __len__(self) -> int:
1343 """
1344 return the length of the RangeIndex
1345 """
1346 return len(self._range)
1347
1348 @property
1349 def size(self) -> int:
1350 return len(self)
1351
1352 @overload # type: ignore[override]
1353 def __getitem__(self, key: int) -> int: ...
1354
1355 @overload
1356 def __getitem__(self, key: slice | np.ndarray | list[int] | list[bool]) -> Self: ...
1357
1358 def __getitem__(self, key: Any) -> Any:
1359 """
1360 Conserve RangeIndex type for scalar and slice keys.
1361 """
1362 key = lib.item_from_zerodim(key)
1363 if key is Ellipsis:
1364 key = slice(None)
1365 if isinstance(key, slice):
1366 return self._getitem_slice(key)
1367 elif is_integer(key):
1368 new_key = int(key)
1369 try:
1370 return self._range[new_key]
1371 except IndexError as err:
1372 raise IndexError(
1373 f"index {key} is out of bounds for axis 0 with size {len(self)}"
1374 ) from err
1375 elif is_scalar(key):
1376 raise IndexError(
1377 "only integers, slices (`:`), "
1378 "ellipsis (`...`), numpy.newaxis (`None`) "
1379 "and integer or boolean "
1380 "arrays are valid indices"
1381 )
1382 elif com.is_bool_indexer(key):
1383 if isinstance(getattr(key, "dtype", None), ExtensionDtype):
1384 key = key.to_numpy(dtype=bool, na_value=False)
1385 else:
1386 key = np.asarray(key, dtype=bool)
1387 check_array_indexer(self._range, key) # type: ignore[arg-type]
1388 key = np.flatnonzero(key)
1389 try:
1390 return self.take(key)
1391 except (TypeError, ValueError):
1392 return super().__getitem__(key)
1393
1394 def _getitem_slice(self, slobj: slice) -> Self:
1395 """
1396 Fastpath for __getitem__ when we know we have a slice.
1397 """
1398 res = self._range[slobj]
1399 return type(self)._simple_new(res, name=self._name)
1400
1401 @unpack_zerodim_and_defer("__floordiv__")
1402 def __floordiv__(self, other: object) -> Index: # type: ignore[override]
1403 if is_integer(other) and other != 0:
1404 if len(self) == 0 or (self.start % other == 0 and self.step % other == 0):
1405 start = self.start // other
1406 step = self.step // other
1407 stop = start + len(self) * step
1408 new_range = range(start, stop, step or 1)
1409 return self._simple_new(new_range, name=self._name)
1410 if len(self) == 1:
1411 start = self.start // other
1412 new_range = range(start, start + 1, 1)
1413 return self._simple_new(new_range, name=self._name)
1414
1415 return super().__floordiv__(other)
1416
1417 # --------------------------------------------------------------------
1418 # Reductions
1419
1420 def all(self, *args: Any, **kwargs: Any) -> bool:
1421 return 0 not in self._range
1422
1423 def any(self, *args: Any, **kwargs: Any) -> bool:
1424 return any(self._range)
1425
1426 # --------------------------------------------------------------------
1427
1428 # error: Return type "RangeIndex | Index" of "round" incompatible with
1429 # return type "RangeIndex" in supertype "Index"
1430 def round(self, decimals: int = 0) -> Self | Index: # type: ignore[override]
1431 """
1432 Round each value in the Index to the given number of decimals.
1433
1434 Parameters
1435 ----------
1436 decimals : int, optional
1437 Number of decimal places to round to. If decimals is negative,
1438 it specifies the number of positions to the left of the decimal point
1439 e.g. ``round(11.0, -1) == 10.0``.
1440
1441 Returns
1442 -------
1443 Index or RangeIndex
1444 A new Index with the rounded values.
1445
1446 Examples
1447 --------
1448 >>> import pandas as pd
1449 >>> idx = pd.RangeIndex(10, 30, 10)
1450 >>> idx.round(decimals=-1)
1451 RangeIndex(start=10, stop=30, step=10)
1452 >>> idx = pd.RangeIndex(10, 15, 1)
1453 >>> idx.round(decimals=-1)
1454 Index([10, 10, 10, 10, 10], dtype='int64')
1455 """
1456 if decimals >= 0:
1457 return self.copy()
1458 elif self.start % 10**-decimals == 0 and self.step % 10**-decimals == 0:
1459 # e.g. RangeIndex(10, 30, 10).round(-1) doesn't need rounding
1460 return self.copy()
1461 else:
1462 return super().round(decimals=decimals)
1463
1464 def _cmp_method(self, other: object, op: Callable) -> Any:
1465 if isinstance(other, RangeIndex) and self._range == other._range:
1466 # Both are immutable so if ._range attr. are equal, shortcut is possible
1467 return super()._cmp_method(self, op)
1468 return super()._cmp_method(other, op)
1469
1470 def _arith_method(self, other: object, op: Callable) -> Index:
1471 """
1472 Parameters
1473 ----------
1474 other : Any
1475 op : callable that accepts 2 params
1476 perform the binary op
1477 """
1478
1479 if isinstance(other, ABCTimedeltaIndex):
1480 # Defer to TimedeltaIndex implementation
1481 return NotImplemented
1482 elif isinstance(other, (timedelta, np.timedelta64)):
1483 # GH#19333 is_integer evaluated True on timedelta64,
1484 # so we need to catch these explicitly
1485 return super()._arith_method(other, op)
1486 elif lib.is_np_dtype(getattr(other, "dtype", None), "m"):
1487 # Must be an np.ndarray; GH#22390
1488 return super()._arith_method(other, op)
1489
1490 if op in [
1491 operator.pow,
1492 ops.rpow,
1493 operator.mod,
1494 ops.rmod,
1495 operator.floordiv,
1496 ops.rfloordiv,
1497 divmod,
1498 ops.rdivmod,
1499 ]:
1500 return super()._arith_method(other, op)
1501
1502 step: Callable | None = None
1503 if op in [operator.mul, ops.rmul, operator.truediv, ops.rtruediv]:
1504 step = op
1505
1506 # TODO: if other is a RangeIndex we may have more efficient options
1507 right = extract_array(other, extract_numpy=True, extract_range=True)
1508 left = self
1509
1510 try:
1511 # apply if we have an override
1512 if step:
1513 with np.errstate(all="ignore"):
1514 rstep = step(left.step, right)
1515
1516 # we don't have a representable op
1517 # so return a base index
1518 if not is_integer(rstep) or not rstep:
1519 raise ValueError
1520
1521 # GH#53255
1522 else:
1523 rstep = -left.step if op == ops.rsub else left.step
1524
1525 with np.errstate(all="ignore"):
1526 rstart = op(left.start, right)
1527 rstop = op(left.stop, right)
1528
1529 res_name = ops.get_op_result_name(self, other) # type: ignore[no-untyped-call]
1530 result = type(self)(rstart, rstop, rstep, name=res_name)
1531
1532 # for compat with numpy / Index with int64 dtype
1533 # even if we can represent as a RangeIndex, return
1534 # as a float64 Index if we have float-like descriptors
1535 if not all(is_integer(x) for x in [rstart, rstop, rstep]):
1536 result = result.astype("float64") # type: ignore[assignment]
1537
1538 return result
1539
1540 except (ValueError, TypeError, ZeroDivisionError):
1541 # test_arithmetic_explicit_conversions
1542 return super()._arith_method(other, op)
1543
1544 def __abs__(self) -> Self | Index:
1545 if len(self) == 0 or self.min() >= 0:
1546 return self.copy()
1547 elif self.max() <= 0:
1548 return -self
1549 else:
1550 return super().__abs__()
1551
1552 def __neg__(self) -> Self:
1553 rng = range(-self.start, -self.stop, -self.step)
1554 return self._simple_new(rng, name=self.name)
1555
1556 def __pos__(self) -> Self:
1557 return self.copy()
1558
1559 def __invert__(self) -> Self:
1560 if len(self) == 0:
1561 return self.copy()
1562 rng = range(~self.start, ~self.stop, -self.step)
1563 return self._simple_new(rng, name=self.name)
1564
1565 # error: Return type "Index" of "take" incompatible with return type
1566 # "RangeIndex" in supertype "Index"
1567 def take( # type: ignore[override]
1568 self,
1569 indices: Sequence[int] | np.ndarray,
1570 axis: Axis = 0,
1571 allow_fill: bool | lib.NoDefault = no_default,
1572 fill_value: object = no_default,
1573 **kwargs: Any,
1574 ) -> Self | Index:
1575 if kwargs:
1576 nv.validate_take((), kwargs)
1577 if is_scalar(indices):
1578 raise TypeError("Expected indices to be array-like")
1579 indices = ensure_platform_int(indices)
1580
1581 if allow_fill is no_default:
1582 if fill_value is None:
1583 # GH#65210: preserve pre-3.1 wrap behavior for this case, but
1584 # warn since the sentinel-based default would otherwise flip
1585 # it into fill-with-NA semantics.
1586 warnings.warn(
1587 "Passing fill_value=None without allow_fill previously "
1588 "used numpy-style wrapping of negative indices. In a "
1589 "future version this will trigger fill semantics "
1590 "(filling -1 entries with the dtype's NA value). Pass "
1591 "allow_fill=False to keep wrapping behavior, or "
1592 "allow_fill=True to opt into fill semantics.",
1593 Pandas4Warning,
1594 stacklevel=find_stack_level(),
1595 )
1596 allow_fill = False
1597 else:
1598 # Default: opt into fill semantics only if fill_value is
1599 # explicit.
1600 allow_fill = fill_value is not no_default
1601
1602 if allow_fill:
1603 # RangeIndex can't hold NA natively; delegate to Index.take which
1604 # will promote dtype as needed.
1605 return super().take(
1606 indices, axis=axis, allow_fill=True, fill_value=fill_value
1607 )
1608
1609 if len(indices) == 0:
1610 return type(self)(_empty_range, name=self.name)
1611 else:
1612 ind_max = indices.max()
1613 if ind_max >= len(self):
1614 raise IndexError(
1615 f"index {ind_max} is out of bounds for axis 0 with size {len(self)}"
1616 )
1617 ind_min = indices.min()
1618 if ind_min < -len(self):
1619 raise IndexError(
1620 f"index {ind_min} is out of bounds for axis 0 with size {len(self)}"
1621 )
1622 taken = indices.astype(self.dtype, casting="safe")
1623 if ind_min < 0:
1624 taken %= len(self)
1625 if self.step != 1:
1626 taken *= self.step
1627 if self.start != 0:
1628 taken += self.start
1629
1630 return self._shallow_copy(taken, name=self.name)
1631
1632 def value_counts(
1633 self,
1634 normalize: bool = False,
1635 sort: bool = True,
1636 ascending: bool = False,
1637 bins: int | None = None,
1638 dropna: bool = True,
1639 ) -> Series:
1640 from pandas import Series
1641
1642 if bins is not None:
1643 return super().value_counts(
1644 normalize=normalize,
1645 sort=sort,
1646 ascending=ascending,
1647 bins=bins,
1648 dropna=dropna,
1649 )
1650 name = "proportion" if normalize else "count"
1651 data: npt.NDArray[np.floating] | npt.NDArray[np.signedinteger] = np.ones(
1652 len(self), dtype=np.int64
1653 )
1654 if normalize:
1655 data = data / len(self)
1656 return Series(data, index=self.copy(), name=name)
1657
1658 @overload
1659 def searchsorted( # type: ignore[overload-overlap] # pyright: ignore[reportOverlappingOverload]
1660 self,
1661 value: ScalarLike_co,
1662 side: Literal["left", "right"] = ...,
1663 sorter: NumpySorter = ...,
1664 ) -> np.intp: ...
1665
1666 @overload
1667 def searchsorted(
1668 self,
1669 value: npt.ArrayLike | ExtensionArray,
1670 side: Literal["left", "right"] = ...,
1671 sorter: NumpySorter = ...,
1672 ) -> npt.NDArray[np.intp]: ...
1673
1674 def searchsorted(
1675 self,
1676 value: NumpyValueArrayLike | ExtensionArray,
1677 side: Literal["left", "right"] = "left",
1678 sorter: NumpySorter | None = None,
1679 ) -> npt.NDArray[np.intp] | np.intp:
1680 if side not in {"left", "right"} or sorter is not None:
1681 return super().searchsorted(value=value, side=side, sorter=sorter)
1682
1683 was_scalar = False
1684 if is_scalar(value):
1685 was_scalar = True
1686 array_value = np.array([value])
1687 else:
1688 array_value = np.asarray(value)
1689 if array_value.dtype.kind not in "iu":
1690 return super().searchsorted(value=value, side=side, sorter=sorter)
1691
1692 if flip := (self.step < 0):
1693 rng = self._range[::-1]
1694 start = rng.start
1695 step = rng.step
1696 shift = side == "right"
1697 else:
1698 start = self.start
1699 step = self.step
1700 shift = side == "left"
1701 result = (array_value - start - int(shift)) // step + 1
1702 if flip:
1703 result = len(self) - result
1704 result = np.maximum(np.minimum(result, len(self)), 0)
1705 if was_scalar:
1706 return maybe_unbox_numpy_scalar(np.intp(result.item()))
1707 return result.astype(np.intp, copy=False)