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