1from __future__ import annotations
2
3from datetime import timedelta
4import operator
5from sys import getsizeof
6from typing import (
7 Any,
8 Callable,
9 Hashable,
10 Iterator,
11 List,
12 cast,
13)
14
15import numpy as np
16
17from pandas._libs import (
18 index as libindex,
19 lib,
20)
21from pandas._libs.algos import unique_deltas
22from pandas._libs.lib import no_default
23from pandas._typing import (
24 Dtype,
25 npt,
26)
27from pandas.compat.numpy import function as nv
28from pandas.util._decorators import (
29 cache_readonly,
30 doc,
31)
32
33from pandas.core.dtypes.common import (
34 ensure_platform_int,
35 ensure_python_int,
36 is_float,
37 is_integer,
38 is_scalar,
39 is_signed_integer_dtype,
40 is_timedelta64_dtype,
41)
42from pandas.core.dtypes.generic import ABCTimedeltaIndex
43
44from pandas.core import ops
45import pandas.core.common as com
46from pandas.core.construction import extract_array
47import pandas.core.indexes.base as ibase
48from pandas.core.indexes.base import (
49 Index,
50 maybe_extract_name,
51)
52from pandas.core.ops.common import unpack_zerodim_and_defer
53
54_empty_range = range(0)
55
56
57class RangeIndex(Index):
58 """
59 Immutable Index implementing a monotonic integer range.
60
61 RangeIndex is a memory-saving special case of an Index limited to representing
62 monotonic ranges with a 64-bit dtype. Using RangeIndex may in some instances
63 improve computing speed.
64
65 This is the default index type used
66 by DataFrame and Series when no explicit index is provided by the user.
67
68 Parameters
69 ----------
70 start : int (default: 0), range, or other RangeIndex instance
71 If int and "stop" is not given, interpreted as "stop" instead.
72 stop : int (default: 0)
73 step : int (default: 1)
74 dtype : np.int64
75 Unused, accepted for homogeneity with other index types.
76 copy : bool, default False
77 Unused, accepted for homogeneity with other index types.
78 name : object, optional
79 Name to be stored in the index.
80
81 Attributes
82 ----------
83 start
84 stop
85 step
86
87 Methods
88 -------
89 from_range
90
91 See Also
92 --------
93 Index : The base pandas Index type.
94 """
95
96 _typ = "rangeindex"
97 _dtype_validation_metadata = (is_signed_integer_dtype, "signed integer")
98 _range: range
99 _values: np.ndarray
100
101 @property
102 def _engine_type(self) -> type[libindex.Int64Engine]:
103 return libindex.Int64Engine
104
105 # --------------------------------------------------------------------
106 # Constructors
107
108 def __new__(
109 cls,
110 start=None,
111 stop=None,
112 step=None,
113 dtype: Dtype | None = None,
114 copy: bool = False,
115 name: Hashable = None,
116 ) -> RangeIndex:
117 cls._validate_dtype(dtype)
118 name = maybe_extract_name(name, start, cls)
119
120 # RangeIndex
121 if isinstance(start, RangeIndex):
122 return start.copy(name=name)
123 elif isinstance(start, range):
124 return cls._simple_new(start, name=name)
125
126 # validate the arguments
127 if com.all_none(start, stop, step):
128 raise TypeError("RangeIndex(...) must be called with integers")
129
130 start = ensure_python_int(start) if start is not None else 0
131
132 if stop is None:
133 start, stop = 0, start
134 else:
135 stop = ensure_python_int(stop)
136
137 step = ensure_python_int(step) if step is not None else 1
138 if step == 0:
139 raise ValueError("Step must not be zero")
140
141 rng = range(start, stop, step)
142 return cls._simple_new(rng, name=name)
143
144 @classmethod
145 def from_range(
146 cls, data: range, name=None, dtype: Dtype | None = None
147 ) -> RangeIndex:
148 """
149 Create RangeIndex from a range object.
150
151 Returns
152 -------
153 RangeIndex
154 """
155 if not isinstance(data, range):
156 raise TypeError(
157 f"{cls.__name__}(...) must be called with object coercible to a "
158 f"range, {repr(data)} was passed"
159 )
160 cls._validate_dtype(dtype)
161 return cls._simple_new(data, name=name)
162
163 # error: Argument 1 of "_simple_new" is incompatible with supertype "Index";
164 # supertype defines the argument type as
165 # "Union[ExtensionArray, ndarray[Any, Any]]" [override]
166 @classmethod
167 def _simple_new( # type: ignore[override]
168 cls, values: range, name: Hashable = None
169 ) -> RangeIndex:
170 result = object.__new__(cls)
171
172 assert isinstance(values, range)
173
174 result._range = values
175 result._name = name
176 result._cache = {}
177 result._reset_identity()
178 result._references = None
179 return result
180
181 @classmethod
182 def _validate_dtype(cls, dtype: Dtype | None) -> None:
183 if dtype is None:
184 return
185
186 validation_func, expected = cls._dtype_validation_metadata
187 if not validation_func(dtype):
188 raise ValueError(
189 f"Incorrect `dtype` passed: expected {expected}, received {dtype}"
190 )
191
192 # --------------------------------------------------------------------
193
194 # error: Return type "Type[Index]" of "_constructor" incompatible with return
195 # type "Type[RangeIndex]" in supertype "Index"
196 @cache_readonly
197 def _constructor(self) -> type[Index]: # type: ignore[override]
198 """return the class to use for construction"""
199 return Index
200
201 # error: Signature of "_data" incompatible with supertype "Index"
202 @cache_readonly
203 def _data(self) -> np.ndarray: # type: ignore[override]
204 """
205 An int array that for performance reasons is created only when needed.
206
207 The constructed array is saved in ``_cache``.
208 """
209 return np.arange(self.start, self.stop, self.step, dtype=np.int64)
210
211 def _get_data_as_items(self):
212 """return a list of tuples of start, stop, step"""
213 rng = self._range
214 return [("start", rng.start), ("stop", rng.stop), ("step", rng.step)]
215
216 def __reduce__(self):
217 d = {"name": self.name}
218 d.update(dict(self._get_data_as_items()))
219 return ibase._new_Index, (type(self), d), None
220
221 # --------------------------------------------------------------------
222 # Rendering Methods
223
224 def _format_attrs(self):
225 """
226 Return a list of tuples of the (attr, formatted_value)
227 """
228 attrs = self._get_data_as_items()
229 if self.name is not None:
230 attrs.append(("name", ibase.default_pprint(self.name)))
231 return attrs
232
233 def _format_data(self, name=None):
234 # we are formatting thru the attributes
235 return None
236
237 def _format_with_header(self, header: list[str], na_rep: str) -> list[str]:
238 # Equivalent to Index implementation, but faster
239 if not len(self._range):
240 return header
241 first_val_str = str(self._range[0])
242 last_val_str = str(self._range[-1])
243 max_length = max(len(first_val_str), len(last_val_str))
244
245 return header + [f"{x:<{max_length}}" for x in self._range]
246
247 # --------------------------------------------------------------------
248
249 @property
250 def start(self) -> int:
251 """
252 The value of the `start` parameter (``0`` if this was not supplied).
253 """
254 # GH 25710
255 return self._range.start
256
257 @property
258 def stop(self) -> int:
259 """
260 The value of the `stop` parameter.
261 """
262 return self._range.stop
263
264 @property
265 def step(self) -> int:
266 """
267 The value of the `step` parameter (``1`` if this was not supplied).
268 """
269 # GH 25710
270 return self._range.step
271
272 @cache_readonly
273 def nbytes(self) -> int:
274 """
275 Return the number of bytes in the underlying data.
276 """
277 rng = self._range
278 return getsizeof(rng) + sum(
279 getsizeof(getattr(rng, attr_name))
280 for attr_name in ["start", "stop", "step"]
281 )
282
283 def memory_usage(self, deep: bool = False) -> int:
284 """
285 Memory usage of my values
286
287 Parameters
288 ----------
289 deep : bool
290 Introspect the data deeply, interrogate
291 `object` dtypes for system-level memory consumption
292
293 Returns
294 -------
295 bytes used
296
297 Notes
298 -----
299 Memory usage does not include memory consumed by elements that
300 are not components of the array if deep=False
301
302 See Also
303 --------
304 numpy.ndarray.nbytes
305 """
306 return self.nbytes
307
308 @property
309 def dtype(self) -> np.dtype:
310 return np.dtype(np.int64)
311
312 @property
313 def is_unique(self) -> bool:
314 """return if the index has unique values"""
315 return True
316
317 @cache_readonly
318 def is_monotonic_increasing(self) -> bool:
319 return self._range.step > 0 or len(self) <= 1
320
321 @cache_readonly
322 def is_monotonic_decreasing(self) -> bool:
323 return self._range.step < 0 or len(self) <= 1
324
325 def __contains__(self, key: Any) -> bool:
326 hash(key)
327 try:
328 key = ensure_python_int(key)
329 except TypeError:
330 return False
331 return key in self._range
332
333 @property
334 def inferred_type(self) -> str:
335 return "integer"
336
337 # --------------------------------------------------------------------
338 # Indexing Methods
339
340 @doc(Index.get_loc)
341 def get_loc(self, key):
342 if is_integer(key) or (is_float(key) and key.is_integer()):
343 new_key = int(key)
344 try:
345 return self._range.index(new_key)
346 except ValueError as err:
347 raise KeyError(key) from err
348 if isinstance(key, Hashable):
349 raise KeyError(key)
350 self._check_indexing_error(key)
351 raise KeyError(key)
352
353 def _get_indexer(
354 self,
355 target: Index,
356 method: str | None = None,
357 limit: int | None = None,
358 tolerance=None,
359 ) -> npt.NDArray[np.intp]:
360 if com.any_not_none(method, tolerance, limit):
361 return super()._get_indexer(
362 target, method=method, tolerance=tolerance, limit=limit
363 )
364
365 if self.step > 0:
366 start, stop, step = self.start, self.stop, self.step
367 else:
368 # GH 28678: work on reversed range for simplicity
369 reverse = self._range[::-1]
370 start, stop, step = reverse.start, reverse.stop, reverse.step
371
372 target_array = np.asarray(target)
373 locs = target_array - start
374 valid = (locs % step == 0) & (locs >= 0) & (target_array < stop)
375 locs[~valid] = -1
376 locs[valid] = locs[valid] / step
377
378 if step != self.step:
379 # We reversed this range: transform to original locs
380 locs[valid] = len(self) - 1 - locs[valid]
381 return ensure_platform_int(locs)
382
383 @cache_readonly
384 def _should_fallback_to_positional(self) -> bool:
385 """
386 Should an integer key be treated as positional?
387 """
388 return False
389
390 # --------------------------------------------------------------------
391
392 def tolist(self) -> list[int]:
393 return list(self._range)
394
395 @doc(Index.__iter__)
396 def __iter__(self) -> Iterator[int]:
397 yield from self._range
398
399 @doc(Index._shallow_copy)
400 def _shallow_copy(self, values, name: Hashable = no_default):
401 name = self.name if name is no_default else name
402
403 if values.dtype.kind == "f":
404 return Index(values, name=name, dtype=np.float64)
405 # GH 46675 & 43885: If values is equally spaced, return a
406 # more memory-compact RangeIndex instead of Index with 64-bit dtype
407 unique_diffs = unique_deltas(values)
408 if len(unique_diffs) == 1 and unique_diffs[0] != 0:
409 diff = unique_diffs[0]
410 new_range = range(values[0], values[-1] + diff, diff)
411 return type(self)._simple_new(new_range, name=name)
412 else:
413 return self._constructor._simple_new(values, name=name)
414
415 def _view(self: RangeIndex) -> RangeIndex:
416 result = type(self)._simple_new(self._range, name=self._name)
417 result._cache = self._cache
418 return result
419
420 @doc(Index.copy)
421 def copy(self, name: Hashable = None, deep: bool = False):
422 name = self._validate_names(name=name, deep=deep)[0]
423 new_index = self._rename(name=name)
424 return new_index
425
426 def _minmax(self, meth: str):
427 no_steps = len(self) - 1
428 if no_steps == -1:
429 return np.nan
430 elif (meth == "min" and self.step > 0) or (meth == "max" and self.step < 0):
431 return self.start
432
433 return self.start + self.step * no_steps
434
435 def min(self, axis=None, skipna: bool = True, *args, **kwargs) -> int:
436 """The minimum value of the RangeIndex"""
437 nv.validate_minmax_axis(axis)
438 nv.validate_min(args, kwargs)
439 return self._minmax("min")
440
441 def max(self, axis=None, skipna: bool = True, *args, **kwargs) -> int:
442 """The maximum value of the RangeIndex"""
443 nv.validate_minmax_axis(axis)
444 nv.validate_max(args, kwargs)
445 return self._minmax("max")
446
447 def argsort(self, *args, **kwargs) -> npt.NDArray[np.intp]:
448 """
449 Returns the indices that would sort the index and its
450 underlying data.
451
452 Returns
453 -------
454 np.ndarray[np.intp]
455
456 See Also
457 --------
458 numpy.ndarray.argsort
459 """
460 ascending = kwargs.pop("ascending", True) # EA compat
461 kwargs.pop("kind", None) # e.g. "mergesort" is irrelevant
462 nv.validate_argsort(args, kwargs)
463
464 if self._range.step > 0:
465 result = np.arange(len(self), dtype=np.intp)
466 else:
467 result = np.arange(len(self) - 1, -1, -1, dtype=np.intp)
468
469 if not ascending:
470 result = result[::-1]
471 return result
472
473 def factorize(
474 self,
475 sort: bool = False,
476 use_na_sentinel: bool = True,
477 ) -> tuple[npt.NDArray[np.intp], RangeIndex]:
478 codes = np.arange(len(self), dtype=np.intp)
479 uniques = self
480 if sort and self.step < 0:
481 codes = codes[::-1]
482 uniques = uniques[::-1]
483 return codes, uniques
484
485 def equals(self, other: object) -> bool:
486 """
487 Determines if two Index objects contain the same elements.
488 """
489 if isinstance(other, RangeIndex):
490 return self._range == other._range
491 return super().equals(other)
492
493 def sort_values(
494 self,
495 return_indexer: bool = False,
496 ascending: bool = True,
497 na_position: str = "last",
498 key: Callable | None = None,
499 ):
500 if key is not None:
501 return super().sort_values(
502 return_indexer=return_indexer,
503 ascending=ascending,
504 na_position=na_position,
505 key=key,
506 )
507 else:
508 sorted_index = self
509 inverse_indexer = False
510 if ascending:
511 if self.step < 0:
512 sorted_index = self[::-1]
513 inverse_indexer = True
514 else:
515 if self.step > 0:
516 sorted_index = self[::-1]
517 inverse_indexer = True
518
519 if return_indexer:
520 if inverse_indexer:
521 rng = range(len(self) - 1, -1, -1)
522 else:
523 rng = range(len(self))
524 return sorted_index, RangeIndex(rng)
525 else:
526 return sorted_index
527
528 # --------------------------------------------------------------------
529 # Set Operations
530
531 def _intersection(self, other: Index, sort: bool = False):
532 # caller is responsible for checking self and other are both non-empty
533
534 if not isinstance(other, RangeIndex):
535 return super()._intersection(other, sort=sort)
536
537 first = self._range[::-1] if self.step < 0 else self._range
538 second = other._range[::-1] if other.step < 0 else other._range
539
540 # check whether intervals intersect
541 # deals with in- and decreasing ranges
542 int_low = max(first.start, second.start)
543 int_high = min(first.stop, second.stop)
544 if int_high <= int_low:
545 return self._simple_new(_empty_range)
546
547 # Method hint: linear Diophantine equation
548 # solve intersection problem
549 # performance hint: for identical step sizes, could use
550 # cheaper alternative
551 gcd, s, _ = self._extended_gcd(first.step, second.step)
552
553 # check whether element sets intersect
554 if (first.start - second.start) % gcd:
555 return self._simple_new(_empty_range)
556
557 # calculate parameters for the RangeIndex describing the
558 # intersection disregarding the lower bounds
559 tmp_start = first.start + (second.start - first.start) * first.step // gcd * s
560 new_step = first.step * second.step // gcd
561 new_range = range(tmp_start, int_high, new_step)
562 new_index = self._simple_new(new_range)
563
564 # adjust index to limiting interval
565 new_start = new_index._min_fitting_element(int_low)
566 new_range = range(new_start, new_index.stop, new_index.step)
567 new_index = self._simple_new(new_range)
568
569 if (self.step < 0 and other.step < 0) is not (new_index.step < 0):
570 new_index = new_index[::-1]
571
572 if sort is None:
573 new_index = new_index.sort_values()
574
575 return new_index
576
577 def _min_fitting_element(self, lower_limit: int) -> int:
578 """Returns the smallest element greater than or equal to the limit"""
579 no_steps = -(-(lower_limit - self.start) // abs(self.step))
580 return self.start + abs(self.step) * no_steps
581
582 def _extended_gcd(self, a: int, b: int) -> tuple[int, int, int]:
583 """
584 Extended Euclidean algorithms to solve Bezout's identity:
585 a*x + b*y = gcd(x, y)
586 Finds one particular solution for x, y: s, t
587 Returns: gcd, s, t
588 """
589 s, old_s = 0, 1
590 t, old_t = 1, 0
591 r, old_r = b, a
592 while r:
593 quotient = old_r // r
594 old_r, r = r, old_r - quotient * r
595 old_s, s = s, old_s - quotient * s
596 old_t, t = t, old_t - quotient * t
597 return old_r, old_s, old_t
598
599 def _range_in_self(self, other: range) -> bool:
600 """Check if other range is contained in self"""
601 # https://stackoverflow.com/a/32481015
602 if not other:
603 return True
604 if not self._range:
605 return False
606 if len(other) > 1 and other.step % self._range.step:
607 return False
608 return other.start in self._range and other[-1] in self._range
609
610 def _union(self, other: Index, sort: bool | None):
611 """
612 Form the union of two Index objects and sorts if possible
613
614 Parameters
615 ----------
616 other : Index or array-like
617
618 sort : bool or None, default None
619 Whether to sort (monotonically increasing) the resulting index.
620 ``sort=None|True`` returns a ``RangeIndex`` if possible or a sorted
621 ``Index`` with a int64 dtype if not.
622 ``sort=False`` can return a ``RangeIndex`` if self is monotonically
623 increasing and other is fully contained in self. Otherwise, returns
624 an unsorted ``Index`` with an int64 dtype.
625
626 Returns
627 -------
628 union : Index
629 """
630 if isinstance(other, RangeIndex):
631 if sort in (None, True) or (
632 sort is False and self.step > 0 and self._range_in_self(other._range)
633 ):
634 # GH 47557: Can still return a RangeIndex
635 # if other range in self and sort=False
636 start_s, step_s = self.start, self.step
637 end_s = self.start + self.step * (len(self) - 1)
638 start_o, step_o = other.start, other.step
639 end_o = other.start + other.step * (len(other) - 1)
640 if self.step < 0:
641 start_s, step_s, end_s = end_s, -step_s, start_s
642 if other.step < 0:
643 start_o, step_o, end_o = end_o, -step_o, start_o
644 if len(self) == 1 and len(other) == 1:
645 step_s = step_o = abs(self.start - other.start)
646 elif len(self) == 1:
647 step_s = step_o
648 elif len(other) == 1:
649 step_o = step_s
650 start_r = min(start_s, start_o)
651 end_r = max(end_s, end_o)
652 if step_o == step_s:
653 if (
654 (start_s - start_o) % step_s == 0
655 and (start_s - end_o) <= step_s
656 and (start_o - end_s) <= step_s
657 ):
658 return type(self)(start_r, end_r + step_s, step_s)
659 if (
660 (step_s % 2 == 0)
661 and (abs(start_s - start_o) == step_s / 2)
662 and (abs(end_s - end_o) == step_s / 2)
663 ):
664 # e.g. range(0, 10, 2) and range(1, 11, 2)
665 # but not range(0, 20, 4) and range(1, 21, 4) GH#44019
666 return type(self)(start_r, end_r + step_s / 2, step_s / 2)
667
668 elif step_o % step_s == 0:
669 if (
670 (start_o - start_s) % step_s == 0
671 and (start_o + step_s >= start_s)
672 and (end_o - step_s <= end_s)
673 ):
674 return type(self)(start_r, end_r + step_s, step_s)
675 elif step_s % step_o == 0:
676 if (
677 (start_s - start_o) % step_o == 0
678 and (start_s + step_o >= start_o)
679 and (end_s - step_o <= end_o)
680 ):
681 return type(self)(start_r, end_r + step_o, step_o)
682
683 return super()._union(other, sort=sort)
684
685 def _difference(self, other, sort=None):
686 # optimized set operation if we have another RangeIndex
687 self._validate_sort_keyword(sort)
688 self._assert_can_do_setop(other)
689 other, result_name = self._convert_can_do_setop(other)
690
691 if not isinstance(other, RangeIndex):
692 return super()._difference(other, sort=sort)
693
694 if sort is not False and self.step < 0:
695 return self[::-1]._difference(other)
696
697 res_name = ops.get_op_result_name(self, other)
698
699 first = self._range[::-1] if self.step < 0 else self._range
700 overlap = self.intersection(other)
701 if overlap.step < 0:
702 overlap = overlap[::-1]
703
704 if len(overlap) == 0:
705 return self.rename(name=res_name)
706 if len(overlap) == len(self):
707 return self[:0].rename(res_name)
708
709 # overlap.step will always be a multiple of self.step (see _intersection)
710
711 if len(overlap) == 1:
712 if overlap[0] == self[0]:
713 return self[1:]
714
715 elif overlap[0] == self[-1]:
716 return self[:-1]
717
718 elif len(self) == 3 and overlap[0] == self[1]:
719 return self[::2]
720
721 else:
722 return super()._difference(other, sort=sort)
723
724 elif len(overlap) == 2 and overlap[0] == first[0] and overlap[-1] == first[-1]:
725 # e.g. range(-8, 20, 7) and range(13, -9, -3)
726 return self[1:-1]
727
728 if overlap.step == first.step:
729 if overlap[0] == first.start:
730 # The difference is everything after the intersection
731 new_rng = range(overlap[-1] + first.step, first.stop, first.step)
732 elif overlap[-1] == first[-1]:
733 # The difference is everything before the intersection
734 new_rng = range(first.start, overlap[0], first.step)
735 elif overlap._range == first[1:-1]:
736 # e.g. range(4) and range(1, 3)
737 step = len(first) - 1
738 new_rng = first[::step]
739 else:
740 # The difference is not range-like
741 # e.g. range(1, 10, 1) and range(3, 7, 1)
742 return super()._difference(other, sort=sort)
743
744 else:
745 # We must have len(self) > 1, bc we ruled out above
746 # len(overlap) == 0 and len(overlap) == len(self)
747 assert len(self) > 1
748
749 if overlap.step == first.step * 2:
750 if overlap[0] == first[0] and overlap[-1] in (first[-1], first[-2]):
751 # e.g. range(1, 10, 1) and range(1, 10, 2)
752 new_rng = first[1::2]
753
754 elif overlap[0] == first[1] and overlap[-1] in (first[-1], first[-2]):
755 # e.g. range(1, 10, 1) and range(2, 10, 2)
756 new_rng = first[::2]
757
758 else:
759 # We can get here with e.g. range(20) and range(0, 10, 2)
760 return super()._difference(other, sort=sort)
761
762 else:
763 # e.g. range(10) and range(0, 10, 3)
764 return super()._difference(other, sort=sort)
765
766 new_index = type(self)._simple_new(new_rng, name=res_name)
767 if first is not self._range:
768 new_index = new_index[::-1]
769
770 return new_index
771
772 def symmetric_difference(self, other, result_name: Hashable = None, sort=None):
773 if not isinstance(other, RangeIndex) or sort is not None:
774 return super().symmetric_difference(other, result_name, sort)
775
776 left = self.difference(other)
777 right = other.difference(self)
778 result = left.union(right)
779
780 if result_name is not None:
781 result = result.rename(result_name)
782 return result
783
784 # --------------------------------------------------------------------
785
786 # error: Return type "Index" of "delete" incompatible with return type
787 # "RangeIndex" in supertype "Index"
788 def delete(self, loc) -> Index: # type: ignore[override]
789 # In some cases we can retain RangeIndex, see also
790 # DatetimeTimedeltaMixin._get_delete_Freq
791 if is_integer(loc):
792 if loc in (0, -len(self)):
793 return self[1:]
794 if loc in (-1, len(self) - 1):
795 return self[:-1]
796 if len(self) == 3 and loc in (1, -2):
797 return self[::2]
798
799 elif lib.is_list_like(loc):
800 slc = lib.maybe_indices_to_slice(np.asarray(loc, dtype=np.intp), len(self))
801
802 if isinstance(slc, slice):
803 # defer to RangeIndex._difference, which is optimized to return
804 # a RangeIndex whenever possible
805 other = self[slc]
806 return self.difference(other, sort=False)
807
808 return super().delete(loc)
809
810 def insert(self, loc: int, item) -> Index:
811 if len(self) and (is_integer(item) or is_float(item)):
812 # We can retain RangeIndex is inserting at the beginning or end,
813 # or right in the middle.
814 rng = self._range
815 if loc == 0 and item == self[0] - self.step:
816 new_rng = range(rng.start - rng.step, rng.stop, rng.step)
817 return type(self)._simple_new(new_rng, name=self.name)
818
819 elif loc == len(self) and item == self[-1] + self.step:
820 new_rng = range(rng.start, rng.stop + rng.step, rng.step)
821 return type(self)._simple_new(new_rng, name=self.name)
822
823 elif len(self) == 2 and item == self[0] + self.step / 2:
824 # e.g. inserting 1 into [0, 2]
825 step = int(self.step / 2)
826 new_rng = range(self.start, self.stop, step)
827 return type(self)._simple_new(new_rng, name=self.name)
828
829 return super().insert(loc, item)
830
831 def _concat(self, indexes: list[Index], name: Hashable) -> Index:
832 """
833 Overriding parent method for the case of all RangeIndex instances.
834
835 When all members of "indexes" are of type RangeIndex: result will be
836 RangeIndex if possible, Index with a int64 dtype otherwise. E.g.:
837 indexes = [RangeIndex(3), RangeIndex(3, 6)] -> RangeIndex(6)
838 indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Index([0,1,2,4,5], dtype='int64')
839 """
840 if not all(isinstance(x, RangeIndex) for x in indexes):
841 return super()._concat(indexes, name)
842
843 elif len(indexes) == 1:
844 return indexes[0]
845
846 rng_indexes = cast(List[RangeIndex], indexes)
847
848 start = step = next_ = None
849
850 # Filter the empty indexes
851 non_empty_indexes = [obj for obj in rng_indexes if len(obj)]
852
853 for obj in non_empty_indexes:
854 rng = obj._range
855
856 if start is None:
857 # This is set by the first non-empty index
858 start = rng.start
859 if step is None and len(rng) > 1:
860 step = rng.step
861 elif step is None:
862 # First non-empty index had only one element
863 if rng.start == start:
864 values = np.concatenate([x._values for x in rng_indexes])
865 result = self._constructor(values)
866 return result.rename(name)
867
868 step = rng.start - start
869
870 non_consecutive = (step != rng.step and len(rng) > 1) or (
871 next_ is not None and rng.start != next_
872 )
873 if non_consecutive:
874 result = self._constructor(
875 np.concatenate([x._values for x in rng_indexes])
876 )
877 return result.rename(name)
878
879 if step is not None:
880 next_ = rng[-1] + step
881
882 if non_empty_indexes:
883 # Get the stop value from "next" or alternatively
884 # from the last non-empty index
885 stop = non_empty_indexes[-1].stop if next_ is None else next_
886 return RangeIndex(start, stop, step).rename(name)
887
888 # Here all "indexes" had 0 length, i.e. were empty.
889 # In this case return an empty range index.
890 return RangeIndex(0, 0).rename(name)
891
892 def __len__(self) -> int:
893 """
894 return the length of the RangeIndex
895 """
896 return len(self._range)
897
898 @property
899 def size(self) -> int:
900 return len(self)
901
902 def __getitem__(self, key):
903 """
904 Conserve RangeIndex type for scalar and slice keys.
905 """
906 if isinstance(key, slice):
907 new_range = self._range[key]
908 return self._simple_new(new_range, name=self._name)
909 elif is_integer(key):
910 new_key = int(key)
911 try:
912 return self._range[new_key]
913 except IndexError as err:
914 raise IndexError(
915 f"index {key} is out of bounds for axis 0 with size {len(self)}"
916 ) from err
917 elif is_scalar(key):
918 raise IndexError(
919 "only integers, slices (`:`), "
920 "ellipsis (`...`), numpy.newaxis (`None`) "
921 "and integer or boolean "
922 "arrays are valid indices"
923 )
924 return super().__getitem__(key)
925
926 def _getitem_slice(self: RangeIndex, slobj: slice) -> RangeIndex:
927 """
928 Fastpath for __getitem__ when we know we have a slice.
929 """
930 res = self._range[slobj]
931 return type(self)._simple_new(res, name=self._name)
932
933 @unpack_zerodim_and_defer("__floordiv__")
934 def __floordiv__(self, other):
935 if is_integer(other) and other != 0:
936 if len(self) == 0 or self.start % other == 0 and self.step % other == 0:
937 start = self.start // other
938 step = self.step // other
939 stop = start + len(self) * step
940 new_range = range(start, stop, step or 1)
941 return self._simple_new(new_range, name=self.name)
942 if len(self) == 1:
943 start = self.start // other
944 new_range = range(start, start + 1, 1)
945 return self._simple_new(new_range, name=self.name)
946
947 return super().__floordiv__(other)
948
949 # --------------------------------------------------------------------
950 # Reductions
951
952 def all(self, *args, **kwargs) -> bool:
953 return 0 not in self._range
954
955 def any(self, *args, **kwargs) -> bool:
956 return any(self._range)
957
958 # --------------------------------------------------------------------
959
960 def _cmp_method(self, other, op):
961 if isinstance(other, RangeIndex) and self._range == other._range:
962 # Both are immutable so if ._range attr. are equal, shortcut is possible
963 return super()._cmp_method(self, op)
964 return super()._cmp_method(other, op)
965
966 def _arith_method(self, other, op):
967 """
968 Parameters
969 ----------
970 other : Any
971 op : callable that accepts 2 params
972 perform the binary op
973 """
974
975 if isinstance(other, ABCTimedeltaIndex):
976 # Defer to TimedeltaIndex implementation
977 return NotImplemented
978 elif isinstance(other, (timedelta, np.timedelta64)):
979 # GH#19333 is_integer evaluated True on timedelta64,
980 # so we need to catch these explicitly
981 return super()._arith_method(other, op)
982 elif is_timedelta64_dtype(other):
983 # Must be an np.ndarray; GH#22390
984 return super()._arith_method(other, op)
985
986 if op in [
987 operator.pow,
988 ops.rpow,
989 operator.mod,
990 ops.rmod,
991 operator.floordiv,
992 ops.rfloordiv,
993 divmod,
994 ops.rdivmod,
995 ]:
996 return super()._arith_method(other, op)
997
998 step: Callable | None = None
999 if op in [operator.mul, ops.rmul, operator.truediv, ops.rtruediv]:
1000 step = op
1001
1002 # TODO: if other is a RangeIndex we may have more efficient options
1003 right = extract_array(other, extract_numpy=True, extract_range=True)
1004 left = self
1005
1006 try:
1007 # apply if we have an override
1008 if step:
1009 with np.errstate(all="ignore"):
1010 rstep = step(left.step, right)
1011
1012 # we don't have a representable op
1013 # so return a base index
1014 if not is_integer(rstep) or not rstep:
1015 raise ValueError
1016
1017 else:
1018 rstep = left.step
1019
1020 with np.errstate(all="ignore"):
1021 rstart = op(left.start, right)
1022 rstop = op(left.stop, right)
1023
1024 res_name = ops.get_op_result_name(self, other)
1025 result = type(self)(rstart, rstop, rstep, name=res_name)
1026
1027 # for compat with numpy / Index with int64 dtype
1028 # even if we can represent as a RangeIndex, return
1029 # as a float64 Index if we have float-like descriptors
1030 if not all(is_integer(x) for x in [rstart, rstop, rstep]):
1031 result = result.astype("float64")
1032
1033 return result
1034
1035 except (ValueError, TypeError, ZeroDivisionError):
1036 # test_arithmetic_explicit_conversions
1037 return super()._arith_method(other, op)