Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/pandas/core/indexes/range.py: 16%

518 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from __future__ import annotations 

2 

3from datetime import timedelta 

4import operator 

5from sys import getsizeof 

6from typing import ( 

7 TYPE_CHECKING, 

8 Any, 

9 Callable, 

10 Hashable, 

11 Iterator, 

12 List, 

13 cast, 

14) 

15import warnings 

16 

17import numpy as np 

18 

19from pandas._libs import ( 

20 index as libindex, 

21 lib, 

22) 

23from pandas._libs.algos import unique_deltas 

24from pandas._libs.lib import no_default 

25from pandas._typing import ( 

26 Dtype, 

27 npt, 

28) 

29from pandas.compat.numpy import function as nv 

30from pandas.util._decorators import ( 

31 cache_readonly, 

32 doc, 

33) 

34from pandas.util._exceptions import find_stack_level 

35 

36from pandas.core.dtypes.common import ( 

37 ensure_platform_int, 

38 ensure_python_int, 

39 is_float, 

40 is_integer, 

41 is_scalar, 

42 is_signed_integer_dtype, 

43 is_timedelta64_dtype, 

44) 

45from pandas.core.dtypes.generic import ABCTimedeltaIndex 

46 

47from pandas.core import ops 

48from pandas.core.algorithms import resolve_na_sentinel 

49import pandas.core.common as com 

50from pandas.core.construction import extract_array 

51import pandas.core.indexes.base as ibase 

52from pandas.core.indexes.base import maybe_extract_name 

53from pandas.core.indexes.numeric import ( 

54 Float64Index, 

55 Int64Index, 

56 NumericIndex, 

57) 

58from pandas.core.ops.common import unpack_zerodim_and_defer 

59 

60if TYPE_CHECKING: 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true

61 from pandas import Index 

62 

63_empty_range = range(0) 

64 

65 

66class RangeIndex(NumericIndex): 

67 """ 

68 Immutable Index implementing a monotonic integer range. 

69 

70 RangeIndex is a memory-saving special case of Int64Index limited to 

71 representing monotonic ranges. Using RangeIndex may in some instances 

72 improve computing speed. 

73 

74 This is the default index type used 

75 by DataFrame and Series when no explicit index is provided by the user. 

76 

77 Parameters 

78 ---------- 

79 start : int (default: 0), range, or other RangeIndex instance 

80 If int and "stop" is not given, interpreted as "stop" instead. 

81 stop : int (default: 0) 

82 step : int (default: 1) 

83 dtype : np.int64 

84 Unused, accepted for homogeneity with other index types. 

85 copy : bool, default False 

86 Unused, accepted for homogeneity with other index types. 

87 name : object, optional 

88 Name to be stored in the index. 

89 

90 Attributes 

91 ---------- 

92 start 

93 stop 

94 step 

95 

96 Methods 

97 ------- 

98 from_range 

99 

100 See Also 

101 -------- 

102 Index : The base pandas Index type. 

103 Int64Index : Index of int64 data. 

104 """ 

105 

106 _typ = "rangeindex" 

107 _dtype_validation_metadata = (is_signed_integer_dtype, "signed integer") 

108 _range: range 

109 _is_backward_compat_public_numeric_index: bool = False 

110 

111 @property 

112 def _engine_type(self) -> type[libindex.Int64Engine]: 

113 return libindex.Int64Engine 

114 

115 # -------------------------------------------------------------------- 

116 # Constructors 

117 

118 def __new__( 

119 cls, 

120 start=None, 

121 stop=None, 

122 step=None, 

123 dtype: Dtype | None = None, 

124 copy: bool = False, 

125 name: Hashable = None, 

126 ) -> RangeIndex: 

127 cls._validate_dtype(dtype) 

128 name = maybe_extract_name(name, start, cls) 

129 

130 # RangeIndex 

131 if isinstance(start, RangeIndex): 

132 return start.copy(name=name) 

133 elif isinstance(start, range): 

134 return cls._simple_new(start, name=name) 

135 

136 # validate the arguments 

137 if com.all_none(start, stop, step): 

138 raise TypeError("RangeIndex(...) must be called with integers") 

139 

140 start = ensure_python_int(start) if start is not None else 0 

141 

142 if stop is None: 

143 start, stop = 0, start 

144 else: 

145 stop = ensure_python_int(stop) 

146 

147 step = ensure_python_int(step) if step is not None else 1 

148 if step == 0: 

149 raise ValueError("Step must not be zero") 

150 

151 rng = range(start, stop, step) 

152 return cls._simple_new(rng, name=name) 

153 

154 @classmethod 

155 def from_range( 

156 cls, data: range, name=None, dtype: Dtype | None = None 

157 ) -> RangeIndex: 

158 """ 

159 Create RangeIndex from a range object. 

160 

161 Returns 

162 ------- 

163 RangeIndex 

164 """ 

165 if not isinstance(data, range): 

166 raise TypeError( 

167 f"{cls.__name__}(...) must be called with object coercible to a " 

168 f"range, {repr(data)} was passed" 

169 ) 

170 cls._validate_dtype(dtype) 

171 return cls._simple_new(data, name=name) 

172 

173 @classmethod 

174 def _simple_new(cls, values: range, name: Hashable = None) -> RangeIndex: 

175 result = object.__new__(cls) 

176 

177 assert isinstance(values, range) 

178 

179 result._range = values 

180 result._name = name 

181 result._cache = {} 

182 result._reset_identity() 

183 return result 

184 

185 # -------------------------------------------------------------------- 

186 

187 # error: Return type "Type[Int64Index]" of "_constructor" incompatible with return 

188 # type "Type[RangeIndex]" in supertype "Index" 

189 @cache_readonly 

190 def _constructor(self) -> type[Int64Index]: # type: ignore[override] 

191 """return the class to use for construction""" 

192 return Int64Index 

193 

194 # error: Signature of "_data" incompatible with supertype "Index" 

195 @cache_readonly 

196 def _data(self) -> np.ndarray: # type: ignore[override] 

197 """ 

198 An int array that for performance reasons is created only when needed. 

199 

200 The constructed array is saved in ``_cache``. 

201 """ 

202 return np.arange(self.start, self.stop, self.step, dtype=np.int64) 

203 

204 def _get_data_as_items(self): 

205 """return a list of tuples of start, stop, step""" 

206 rng = self._range 

207 return [("start", rng.start), ("stop", rng.stop), ("step", rng.step)] 

208 

209 def __reduce__(self): 

210 d = {"name": self.name} 

211 d.update(dict(self._get_data_as_items())) 

212 return ibase._new_Index, (type(self), d), None 

213 

214 # -------------------------------------------------------------------- 

215 # Rendering Methods 

216 

217 def _format_attrs(self): 

218 """ 

219 Return a list of tuples of the (attr, formatted_value) 

220 """ 

221 attrs = self._get_data_as_items() 

222 if self.name is not None: 

223 attrs.append(("name", ibase.default_pprint(self.name))) 

224 return attrs 

225 

226 def _format_data(self, name=None): 

227 # we are formatting thru the attributes 

228 return None 

229 

230 def _format_with_header(self, header: list[str], na_rep: str) -> list[str]: 

231 # Equivalent to Index implementation, but faster 

232 if not len(self._range): 

233 return header 

234 first_val_str = str(self._range[0]) 

235 last_val_str = str(self._range[-1]) 

236 max_length = max(len(first_val_str), len(last_val_str)) 

237 

238 return header + [f"{x:<{max_length}}" for x in self._range] 

239 

240 # -------------------------------------------------------------------- 

241 _deprecation_message = ( 

242 "RangeIndex.{} is deprecated and will be " 

243 "removed in a future version. Use RangeIndex.{} " 

244 "instead" 

245 ) 

246 

247 @property 

248 def start(self) -> int: 

249 """ 

250 The value of the `start` parameter (``0`` if this was not supplied). 

251 """ 

252 # GH 25710 

253 return self._range.start 

254 

255 @property 

256 def _start(self) -> int: 

257 """ 

258 The value of the `start` parameter (``0`` if this was not supplied). 

259 

260 .. deprecated:: 0.25.0 

261 Use ``start`` instead. 

262 """ 

263 warnings.warn( 

264 self._deprecation_message.format("_start", "start"), 

265 FutureWarning, 

266 stacklevel=find_stack_level(), 

267 ) 

268 return self.start 

269 

270 @property 

271 def stop(self) -> int: 

272 """ 

273 The value of the `stop` parameter. 

274 """ 

275 return self._range.stop 

276 

277 @property 

278 def _stop(self) -> int: 

279 """ 

280 The value of the `stop` parameter. 

281 

282 .. deprecated:: 0.25.0 

283 Use ``stop`` instead. 

284 """ 

285 # GH 25710 

286 warnings.warn( 

287 self._deprecation_message.format("_stop", "stop"), 

288 FutureWarning, 

289 stacklevel=find_stack_level(), 

290 ) 

291 return self.stop 

292 

293 @property 

294 def step(self) -> int: 

295 """ 

296 The value of the `step` parameter (``1`` if this was not supplied). 

297 """ 

298 # GH 25710 

299 return self._range.step 

300 

301 @property 

302 def _step(self) -> int: 

303 """ 

304 The value of the `step` parameter (``1`` if this was not supplied). 

305 

306 .. deprecated:: 0.25.0 

307 Use ``step`` instead. 

308 """ 

309 # GH 25710 

310 warnings.warn( 

311 self._deprecation_message.format("_step", "step"), 

312 FutureWarning, 

313 stacklevel=find_stack_level(), 

314 ) 

315 return self.step 

316 

317 @cache_readonly 

318 def nbytes(self) -> int: 

319 """ 

320 Return the number of bytes in the underlying data. 

321 """ 

322 rng = self._range 

323 return getsizeof(rng) + sum( 

324 getsizeof(getattr(rng, attr_name)) 

325 for attr_name in ["start", "stop", "step"] 

326 ) 

327 

328 def memory_usage(self, deep: bool = False) -> int: 

329 """ 

330 Memory usage of my values 

331 

332 Parameters 

333 ---------- 

334 deep : bool 

335 Introspect the data deeply, interrogate 

336 `object` dtypes for system-level memory consumption 

337 

338 Returns 

339 ------- 

340 bytes used 

341 

342 Notes 

343 ----- 

344 Memory usage does not include memory consumed by elements that 

345 are not components of the array if deep=False 

346 

347 See Also 

348 -------- 

349 numpy.ndarray.nbytes 

350 """ 

351 return self.nbytes 

352 

353 @property 

354 def dtype(self) -> np.dtype: 

355 return np.dtype(np.int64) 

356 

357 @property 

358 def is_unique(self) -> bool: 

359 """return if the index has unique values""" 

360 return True 

361 

362 @cache_readonly 

363 def is_monotonic_increasing(self) -> bool: 

364 return self._range.step > 0 or len(self) <= 1 

365 

366 @cache_readonly 

367 def is_monotonic_decreasing(self) -> bool: 

368 return self._range.step < 0 or len(self) <= 1 

369 

370 def __contains__(self, key: Any) -> bool: 

371 hash(key) 

372 try: 

373 key = ensure_python_int(key) 

374 except TypeError: 

375 return False 

376 return key in self._range 

377 

378 @property 

379 def inferred_type(self) -> str: 

380 return "integer" 

381 

382 # -------------------------------------------------------------------- 

383 # Indexing Methods 

384 

385 @doc(Int64Index.get_loc) 

386 def get_loc(self, key, method=None, tolerance=None): 

387 if method is None and tolerance is None: 

388 if is_integer(key) or (is_float(key) and key.is_integer()): 

389 new_key = int(key) 

390 try: 

391 return self._range.index(new_key) 

392 except ValueError as err: 

393 raise KeyError(key) from err 

394 self._check_indexing_error(key) 

395 raise KeyError(key) 

396 return super().get_loc(key, method=method, tolerance=tolerance) 

397 

398 def _get_indexer( 

399 self, 

400 target: Index, 

401 method: str | None = None, 

402 limit: int | None = None, 

403 tolerance=None, 

404 ) -> npt.NDArray[np.intp]: 

405 if com.any_not_none(method, tolerance, limit): 

406 return super()._get_indexer( 

407 target, method=method, tolerance=tolerance, limit=limit 

408 ) 

409 

410 if self.step > 0: 

411 start, stop, step = self.start, self.stop, self.step 

412 else: 

413 # GH 28678: work on reversed range for simplicity 

414 reverse = self._range[::-1] 

415 start, stop, step = reverse.start, reverse.stop, reverse.step 

416 

417 target_array = np.asarray(target) 

418 locs = target_array - start 

419 valid = (locs % step == 0) & (locs >= 0) & (target_array < stop) 

420 locs[~valid] = -1 

421 locs[valid] = locs[valid] / step 

422 

423 if step != self.step: 

424 # We reversed this range: transform to original locs 

425 locs[valid] = len(self) - 1 - locs[valid] 

426 return ensure_platform_int(locs) 

427 

428 # -------------------------------------------------------------------- 

429 

430 def tolist(self) -> list[int]: 

431 return list(self._range) 

432 

433 @doc(Int64Index.__iter__) 

434 def __iter__(self) -> Iterator[int]: 

435 yield from self._range 

436 

437 @doc(Int64Index._shallow_copy) 

438 def _shallow_copy(self, values, name: Hashable = no_default): 

439 name = self.name if name is no_default else name 

440 

441 if values.dtype.kind == "f": 

442 return Float64Index(values, name=name) 

443 # GH 46675 & 43885: If values is equally spaced, return a 

444 # more memory-compact RangeIndex instead of Int64Index 

445 unique_diffs = unique_deltas(values) 

446 if len(unique_diffs) == 1 and unique_diffs[0] != 0: 

447 diff = unique_diffs[0] 

448 new_range = range(values[0], values[-1] + diff, diff) 

449 return type(self)._simple_new(new_range, name=name) 

450 else: 

451 return Int64Index._simple_new(values, name=name) 

452 

453 def _view(self: RangeIndex) -> RangeIndex: 

454 result = type(self)._simple_new(self._range, name=self._name) 

455 result._cache = self._cache 

456 return result 

457 

458 @doc(Int64Index.copy) 

459 def copy( 

460 self, 

461 name: Hashable = None, 

462 deep: bool = False, 

463 dtype: Dtype | None = None, 

464 names=None, 

465 ): 

466 name = self._validate_names(name=name, names=names, deep=deep)[0] 

467 new_index = self._rename(name=name) 

468 

469 if dtype: 

470 warnings.warn( 

471 "parameter dtype is deprecated and will be removed in a future " 

472 "version. Use the astype method instead.", 

473 FutureWarning, 

474 stacklevel=find_stack_level(), 

475 ) 

476 new_index = new_index.astype(dtype) 

477 return new_index 

478 

479 def _minmax(self, meth: str): 

480 no_steps = len(self) - 1 

481 if no_steps == -1: 

482 return np.nan 

483 elif (meth == "min" and self.step > 0) or (meth == "max" and self.step < 0): 

484 return self.start 

485 

486 return self.start + self.step * no_steps 

487 

488 def min(self, axis=None, skipna: bool = True, *args, **kwargs) -> int: 

489 """The minimum value of the RangeIndex""" 

490 nv.validate_minmax_axis(axis) 

491 nv.validate_min(args, kwargs) 

492 return self._minmax("min") 

493 

494 def max(self, axis=None, skipna: bool = True, *args, **kwargs) -> int: 

495 """The maximum value of the RangeIndex""" 

496 nv.validate_minmax_axis(axis) 

497 nv.validate_max(args, kwargs) 

498 return self._minmax("max") 

499 

500 def argsort(self, *args, **kwargs) -> npt.NDArray[np.intp]: 

501 """ 

502 Returns the indices that would sort the index and its 

503 underlying data. 

504 

505 Returns 

506 ------- 

507 np.ndarray[np.intp] 

508 

509 See Also 

510 -------- 

511 numpy.ndarray.argsort 

512 """ 

513 ascending = kwargs.pop("ascending", True) # EA compat 

514 kwargs.pop("kind", None) # e.g. "mergesort" is irrelevant 

515 nv.validate_argsort(args, kwargs) 

516 

517 if self._range.step > 0: 

518 result = np.arange(len(self), dtype=np.intp) 

519 else: 

520 result = np.arange(len(self) - 1, -1, -1, dtype=np.intp) 

521 

522 if not ascending: 

523 result = result[::-1] 

524 return result 

525 

526 def factorize( 

527 self, 

528 sort: bool = False, 

529 na_sentinel: int | lib.NoDefault = lib.no_default, 

530 use_na_sentinel: bool | lib.NoDefault = lib.no_default, 

531 ) -> tuple[npt.NDArray[np.intp], RangeIndex]: 

532 # resolve to emit warning if appropriate 

533 resolve_na_sentinel(na_sentinel, use_na_sentinel) 

534 codes = np.arange(len(self), dtype=np.intp) 

535 uniques = self 

536 if sort and self.step < 0: 

537 codes = codes[::-1] 

538 uniques = uniques[::-1] 

539 return codes, uniques 

540 

541 def equals(self, other: object) -> bool: 

542 """ 

543 Determines if two Index objects contain the same elements. 

544 """ 

545 if isinstance(other, RangeIndex): 

546 return self._range == other._range 

547 return super().equals(other) 

548 

549 def sort_values( 

550 self, 

551 return_indexer: bool = False, 

552 ascending: bool = True, 

553 na_position: str = "last", 

554 key: Callable | None = None, 

555 ): 

556 sorted_index = self 

557 indexer = RangeIndex(range(len(self))) 

558 if key is not None: 

559 return super().sort_values( 

560 return_indexer=return_indexer, 

561 ascending=ascending, 

562 na_position=na_position, 

563 key=key, 

564 ) 

565 else: 

566 sorted_index = self 

567 if ascending: 

568 if self.step < 0: 

569 sorted_index = self[::-1] 

570 indexer = indexer[::-1] 

571 else: 

572 if self.step > 0: 

573 sorted_index = self[::-1] 

574 indexer = indexer = indexer[::-1] 

575 

576 if return_indexer: 

577 return sorted_index, indexer 

578 else: 

579 return sorted_index 

580 

581 # -------------------------------------------------------------------- 

582 # Set Operations 

583 

584 def _intersection(self, other: Index, sort=False): 

585 # caller is responsible for checking self and other are both non-empty 

586 

587 if not isinstance(other, RangeIndex): 

588 # Int64Index 

589 return super()._intersection(other, sort=sort) 

590 

591 first = self._range[::-1] if self.step < 0 else self._range 

592 second = other._range[::-1] if other.step < 0 else other._range 

593 

594 # check whether intervals intersect 

595 # deals with in- and decreasing ranges 

596 int_low = max(first.start, second.start) 

597 int_high = min(first.stop, second.stop) 

598 if int_high <= int_low: 

599 return self._simple_new(_empty_range) 

600 

601 # Method hint: linear Diophantine equation 

602 # solve intersection problem 

603 # performance hint: for identical step sizes, could use 

604 # cheaper alternative 

605 gcd, s, _ = self._extended_gcd(first.step, second.step) 

606 

607 # check whether element sets intersect 

608 if (first.start - second.start) % gcd: 

609 return self._simple_new(_empty_range) 

610 

611 # calculate parameters for the RangeIndex describing the 

612 # intersection disregarding the lower bounds 

613 tmp_start = first.start + (second.start - first.start) * first.step // gcd * s 

614 new_step = first.step * second.step // gcd 

615 new_range = range(tmp_start, int_high, new_step) 

616 new_index = self._simple_new(new_range) 

617 

618 # adjust index to limiting interval 

619 new_start = new_index._min_fitting_element(int_low) 

620 new_range = range(new_start, new_index.stop, new_index.step) 

621 new_index = self._simple_new(new_range) 

622 

623 if (self.step < 0 and other.step < 0) is not (new_index.step < 0): 

624 new_index = new_index[::-1] 

625 

626 if sort is None: 

627 new_index = new_index.sort_values() 

628 

629 return new_index 

630 

631 def _min_fitting_element(self, lower_limit: int) -> int: 

632 """Returns the smallest element greater than or equal to the limit""" 

633 no_steps = -(-(lower_limit - self.start) // abs(self.step)) 

634 return self.start + abs(self.step) * no_steps 

635 

636 def _extended_gcd(self, a: int, b: int) -> tuple[int, int, int]: 

637 """ 

638 Extended Euclidean algorithms to solve Bezout's identity: 

639 a*x + b*y = gcd(x, y) 

640 Finds one particular solution for x, y: s, t 

641 Returns: gcd, s, t 

642 """ 

643 s, old_s = 0, 1 

644 t, old_t = 1, 0 

645 r, old_r = b, a 

646 while r: 

647 quotient = old_r // r 

648 old_r, r = r, old_r - quotient * r 

649 old_s, s = s, old_s - quotient * s 

650 old_t, t = t, old_t - quotient * t 

651 return old_r, old_s, old_t 

652 

653 def _range_in_self(self, other: range) -> bool: 

654 """Check if other range is contained in self""" 

655 # https://stackoverflow.com/a/32481015 

656 if not other: 

657 return True 

658 if not self._range: 

659 return False 

660 if len(other) > 1 and other.step % self._range.step: 

661 return False 

662 return other.start in self._range and other[-1] in self._range 

663 

664 def _union(self, other: Index, sort): 

665 """ 

666 Form the union of two Index objects and sorts if possible 

667 

668 Parameters 

669 ---------- 

670 other : Index or array-like 

671 

672 sort : False or None, default None 

673 Whether to sort (monotonically increasing) the resulting index. 

674 ``sort=None`` returns a ``RangeIndex`` if possible or a sorted 

675 ``Int64Index`` if not. 

676 ``sort=False`` can return a ``RangeIndex`` if self is monotonically 

677 increasing and other is fully contained in self. Otherwise, returns 

678 an unsorted ``Int64Index`` 

679 

680 .. versionadded:: 0.25.0 

681 

682 Returns 

683 ------- 

684 union : Index 

685 """ 

686 if isinstance(other, RangeIndex): 

687 if sort is None or ( 

688 sort is False and self.step > 0 and self._range_in_self(other._range) 

689 ): 

690 # GH 47557: Can still return a RangeIndex 

691 # if other range in self and sort=False 

692 start_s, step_s = self.start, self.step 

693 end_s = self.start + self.step * (len(self) - 1) 

694 start_o, step_o = other.start, other.step 

695 end_o = other.start + other.step * (len(other) - 1) 

696 if self.step < 0: 

697 start_s, step_s, end_s = end_s, -step_s, start_s 

698 if other.step < 0: 

699 start_o, step_o, end_o = end_o, -step_o, start_o 

700 if len(self) == 1 and len(other) == 1: 

701 step_s = step_o = abs(self.start - other.start) 

702 elif len(self) == 1: 

703 step_s = step_o 

704 elif len(other) == 1: 

705 step_o = step_s 

706 start_r = min(start_s, start_o) 

707 end_r = max(end_s, end_o) 

708 if step_o == step_s: 

709 if ( 

710 (start_s - start_o) % step_s == 0 

711 and (start_s - end_o) <= step_s 

712 and (start_o - end_s) <= step_s 

713 ): 

714 return type(self)(start_r, end_r + step_s, step_s) 

715 if ( 

716 (step_s % 2 == 0) 

717 and (abs(start_s - start_o) == step_s / 2) 

718 and (abs(end_s - end_o) == step_s / 2) 

719 ): 

720 # e.g. range(0, 10, 2) and range(1, 11, 2) 

721 # but not range(0, 20, 4) and range(1, 21, 4) GH#44019 

722 return type(self)(start_r, end_r + step_s / 2, step_s / 2) 

723 

724 elif step_o % step_s == 0: 

725 if ( 

726 (start_o - start_s) % step_s == 0 

727 and (start_o + step_s >= start_s) 

728 and (end_o - step_s <= end_s) 

729 ): 

730 return type(self)(start_r, end_r + step_s, step_s) 

731 elif step_s % step_o == 0: 

732 if ( 

733 (start_s - start_o) % step_o == 0 

734 and (start_s + step_o >= start_o) 

735 and (end_s - step_o <= end_o) 

736 ): 

737 return type(self)(start_r, end_r + step_o, step_o) 

738 

739 return super()._union(other, sort=sort) 

740 

741 def _difference(self, other, sort=None): 

742 # optimized set operation if we have another RangeIndex 

743 self._validate_sort_keyword(sort) 

744 self._assert_can_do_setop(other) 

745 other, result_name = self._convert_can_do_setop(other) 

746 

747 if not isinstance(other, RangeIndex): 

748 return super()._difference(other, sort=sort) 

749 

750 if sort is None and self.step < 0: 

751 return self[::-1]._difference(other) 

752 

753 res_name = ops.get_op_result_name(self, other) 

754 

755 first = self._range[::-1] if self.step < 0 else self._range 

756 overlap = self.intersection(other) 

757 if overlap.step < 0: 

758 overlap = overlap[::-1] 

759 

760 if len(overlap) == 0: 

761 return self.rename(name=res_name) 

762 if len(overlap) == len(self): 

763 return self[:0].rename(res_name) 

764 

765 # overlap.step will always be a multiple of self.step (see _intersection) 

766 

767 if len(overlap) == 1: 

768 if overlap[0] == self[0]: 

769 return self[1:] 

770 

771 elif overlap[0] == self[-1]: 

772 return self[:-1] 

773 

774 elif len(self) == 3 and overlap[0] == self[1]: 

775 return self[::2] 

776 

777 else: 

778 return super()._difference(other, sort=sort) 

779 

780 elif len(overlap) == 2 and overlap[0] == first[0] and overlap[-1] == first[-1]: 

781 # e.g. range(-8, 20, 7) and range(13, -9, -3) 

782 return self[1:-1] 

783 

784 if overlap.step == first.step: 

785 if overlap[0] == first.start: 

786 # The difference is everything after the intersection 

787 new_rng = range(overlap[-1] + first.step, first.stop, first.step) 

788 elif overlap[-1] == first[-1]: 

789 # The difference is everything before the intersection 

790 new_rng = range(first.start, overlap[0], first.step) 

791 elif overlap._range == first[1:-1]: 

792 # e.g. range(4) and range(1, 3) 

793 step = len(first) - 1 

794 new_rng = first[::step] 

795 else: 

796 # The difference is not range-like 

797 # e.g. range(1, 10, 1) and range(3, 7, 1) 

798 return super()._difference(other, sort=sort) 

799 

800 else: 

801 # We must have len(self) > 1, bc we ruled out above 

802 # len(overlap) == 0 and len(overlap) == len(self) 

803 assert len(self) > 1 

804 

805 if overlap.step == first.step * 2: 

806 if overlap[0] == first[0] and overlap[-1] in (first[-1], first[-2]): 

807 # e.g. range(1, 10, 1) and range(1, 10, 2) 

808 new_rng = first[1::2] 

809 

810 elif overlap[0] == first[1] and overlap[-1] in (first[-1], first[-2]): 

811 # e.g. range(1, 10, 1) and range(2, 10, 2) 

812 new_rng = first[::2] 

813 

814 else: 

815 # We can get here with e.g. range(20) and range(0, 10, 2) 

816 return super()._difference(other, sort=sort) 

817 

818 else: 

819 # e.g. range(10) and range(0, 10, 3) 

820 return super()._difference(other, sort=sort) 

821 

822 new_index = type(self)._simple_new(new_rng, name=res_name) 

823 if first is not self._range: 

824 new_index = new_index[::-1] 

825 

826 return new_index 

827 

828 def symmetric_difference(self, other, result_name: Hashable = None, sort=None): 

829 if not isinstance(other, RangeIndex) or sort is not None: 

830 return super().symmetric_difference(other, result_name, sort) 

831 

832 left = self.difference(other) 

833 right = other.difference(self) 

834 result = left.union(right) 

835 

836 if result_name is not None: 

837 result = result.rename(result_name) 

838 return result 

839 

840 # -------------------------------------------------------------------- 

841 

842 # error: Return type "Index" of "delete" incompatible with return type 

843 # "RangeIndex" in supertype "Index" 

844 def delete(self, loc) -> Index: # type: ignore[override] 

845 # In some cases we can retain RangeIndex, see also 

846 # DatetimeTimedeltaMixin._get_delete_Freq 

847 if is_integer(loc): 

848 if loc == 0 or loc == -len(self): 

849 return self[1:] 

850 if loc == -1 or loc == len(self) - 1: 

851 return self[:-1] 

852 if len(self) == 3 and (loc == 1 or loc == -2): 

853 return self[::2] 

854 

855 elif lib.is_list_like(loc): 

856 slc = lib.maybe_indices_to_slice(np.asarray(loc, dtype=np.intp), len(self)) 

857 

858 if isinstance(slc, slice): 

859 # defer to RangeIndex._difference, which is optimized to return 

860 # a RangeIndex whenever possible 

861 other = self[slc] 

862 return self.difference(other, sort=False) 

863 

864 return super().delete(loc) 

865 

866 def insert(self, loc: int, item) -> Index: 

867 if len(self) and (is_integer(item) or is_float(item)): 

868 # We can retain RangeIndex is inserting at the beginning or end, 

869 # or right in the middle. 

870 rng = self._range 

871 if loc == 0 and item == self[0] - self.step: 

872 new_rng = range(rng.start - rng.step, rng.stop, rng.step) 

873 return type(self)._simple_new(new_rng, name=self.name) 

874 

875 elif loc == len(self) and item == self[-1] + self.step: 

876 new_rng = range(rng.start, rng.stop + rng.step, rng.step) 

877 return type(self)._simple_new(new_rng, name=self.name) 

878 

879 elif len(self) == 2 and item == self[0] + self.step / 2: 

880 # e.g. inserting 1 into [0, 2] 

881 step = int(self.step / 2) 

882 new_rng = range(self.start, self.stop, step) 

883 return type(self)._simple_new(new_rng, name=self.name) 

884 

885 return super().insert(loc, item) 

886 

887 def _concat(self, indexes: list[Index], name: Hashable) -> Index: 

888 """ 

889 Overriding parent method for the case of all RangeIndex instances. 

890 

891 When all members of "indexes" are of type RangeIndex: result will be 

892 RangeIndex if possible, Int64Index otherwise. E.g.: 

893 indexes = [RangeIndex(3), RangeIndex(3, 6)] -> RangeIndex(6) 

894 indexes = [RangeIndex(3), RangeIndex(4, 6)] -> Int64Index([0,1,2,4,5]) 

895 """ 

896 if not all(isinstance(x, RangeIndex) for x in indexes): 

897 return super()._concat(indexes, name) 

898 

899 elif len(indexes) == 1: 

900 return indexes[0] 

901 

902 rng_indexes = cast(List[RangeIndex], indexes) 

903 

904 start = step = next_ = None 

905 

906 # Filter the empty indexes 

907 non_empty_indexes = [obj for obj in rng_indexes if len(obj)] 

908 

909 for obj in non_empty_indexes: 

910 rng = obj._range 

911 

912 if start is None: 

913 # This is set by the first non-empty index 

914 start = rng.start 

915 if step is None and len(rng) > 1: 

916 step = rng.step 

917 elif step is None: 

918 # First non-empty index had only one element 

919 if rng.start == start: 

920 values = np.concatenate([x._values for x in rng_indexes]) 

921 result = Int64Index(values) 

922 return result.rename(name) 

923 

924 step = rng.start - start 

925 

926 non_consecutive = (step != rng.step and len(rng) > 1) or ( 

927 next_ is not None and rng.start != next_ 

928 ) 

929 if non_consecutive: 

930 result = Int64Index(np.concatenate([x._values for x in rng_indexes])) 

931 return result.rename(name) 

932 

933 if step is not None: 

934 next_ = rng[-1] + step 

935 

936 if non_empty_indexes: 

937 # Get the stop value from "next" or alternatively 

938 # from the last non-empty index 

939 stop = non_empty_indexes[-1].stop if next_ is None else next_ 

940 return RangeIndex(start, stop, step).rename(name) 

941 

942 # Here all "indexes" had 0 length, i.e. were empty. 

943 # In this case return an empty range index. 

944 return RangeIndex(0, 0).rename(name) 

945 

946 def __len__(self) -> int: 

947 """ 

948 return the length of the RangeIndex 

949 """ 

950 return len(self._range) 

951 

952 @property 

953 def size(self) -> int: 

954 return len(self) 

955 

956 def __getitem__(self, key): 

957 """ 

958 Conserve RangeIndex type for scalar and slice keys. 

959 """ 

960 if isinstance(key, slice): 

961 new_range = self._range[key] 

962 return self._simple_new(new_range, name=self._name) 

963 elif is_integer(key): 

964 new_key = int(key) 

965 try: 

966 return self._range[new_key] 

967 except IndexError as err: 

968 raise IndexError( 

969 f"index {key} is out of bounds for axis 0 with size {len(self)}" 

970 ) from err 

971 elif is_scalar(key): 

972 raise IndexError( 

973 "only integers, slices (`:`), " 

974 "ellipsis (`...`), numpy.newaxis (`None`) " 

975 "and integer or boolean " 

976 "arrays are valid indices" 

977 ) 

978 # fall back to Int64Index 

979 return super().__getitem__(key) 

980 

981 def _getitem_slice(self: RangeIndex, slobj: slice) -> RangeIndex: 

982 """ 

983 Fastpath for __getitem__ when we know we have a slice. 

984 """ 

985 res = self._range[slobj] 

986 return type(self)._simple_new(res, name=self._name) 

987 

988 @unpack_zerodim_and_defer("__floordiv__") 

989 def __floordiv__(self, other): 

990 

991 if is_integer(other) and other != 0: 

992 if len(self) == 0 or self.start % other == 0 and self.step % other == 0: 

993 start = self.start // other 

994 step = self.step // other 

995 stop = start + len(self) * step 

996 new_range = range(start, stop, step or 1) 

997 return self._simple_new(new_range, name=self.name) 

998 if len(self) == 1: 

999 start = self.start // other 

1000 new_range = range(start, start + 1, 1) 

1001 return self._simple_new(new_range, name=self.name) 

1002 

1003 return super().__floordiv__(other) 

1004 

1005 # -------------------------------------------------------------------- 

1006 # Reductions 

1007 

1008 def all(self, *args, **kwargs) -> bool: 

1009 return 0 not in self._range 

1010 

1011 def any(self, *args, **kwargs) -> bool: 

1012 return any(self._range) 

1013 

1014 # -------------------------------------------------------------------- 

1015 

1016 def _cmp_method(self, other, op): 

1017 if isinstance(other, RangeIndex) and self._range == other._range: 

1018 # Both are immutable so if ._range attr. are equal, shortcut is possible 

1019 return super()._cmp_method(self, op) 

1020 return super()._cmp_method(other, op) 

1021 

1022 def _arith_method(self, other, op): 

1023 """ 

1024 Parameters 

1025 ---------- 

1026 other : Any 

1027 op : callable that accepts 2 params 

1028 perform the binary op 

1029 """ 

1030 

1031 if isinstance(other, ABCTimedeltaIndex): 

1032 # Defer to TimedeltaIndex implementation 

1033 return NotImplemented 

1034 elif isinstance(other, (timedelta, np.timedelta64)): 

1035 # GH#19333 is_integer evaluated True on timedelta64, 

1036 # so we need to catch these explicitly 

1037 return super()._arith_method(other, op) 

1038 elif is_timedelta64_dtype(other): 

1039 # Must be an np.ndarray; GH#22390 

1040 return super()._arith_method(other, op) 

1041 

1042 if op in [ 

1043 operator.pow, 

1044 ops.rpow, 

1045 operator.mod, 

1046 ops.rmod, 

1047 operator.floordiv, 

1048 ops.rfloordiv, 

1049 divmod, 

1050 ops.rdivmod, 

1051 ]: 

1052 return super()._arith_method(other, op) 

1053 

1054 step: Callable | None = None 

1055 if op in [operator.mul, ops.rmul, operator.truediv, ops.rtruediv]: 

1056 step = op 

1057 

1058 # TODO: if other is a RangeIndex we may have more efficient options 

1059 right = extract_array(other, extract_numpy=True, extract_range=True) 

1060 left = self 

1061 

1062 try: 

1063 # apply if we have an override 

1064 if step: 

1065 with np.errstate(all="ignore"): 

1066 rstep = step(left.step, right) 

1067 

1068 # we don't have a representable op 

1069 # so return a base index 

1070 if not is_integer(rstep) or not rstep: 

1071 raise ValueError 

1072 

1073 else: 

1074 rstep = left.step 

1075 

1076 with np.errstate(all="ignore"): 

1077 rstart = op(left.start, right) 

1078 rstop = op(left.stop, right) 

1079 

1080 res_name = ops.get_op_result_name(self, other) 

1081 result = type(self)(rstart, rstop, rstep, name=res_name) 

1082 

1083 # for compat with numpy / Int64Index 

1084 # even if we can represent as a RangeIndex, return 

1085 # as a Float64Index if we have float-like descriptors 

1086 if not all(is_integer(x) for x in [rstart, rstop, rstep]): 

1087 result = result.astype("float64") 

1088 

1089 return result 

1090 

1091 except (ValueError, TypeError, ZeroDivisionError): 

1092 # Defer to Int64Index implementation 

1093 # test_arithmetic_explicit_conversions 

1094 return super()._arith_method(other, op)