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