Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/core/indexes/datetimelike.py: 26%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

388 statements  

1""" 

2Base and utility classes for tseries type pandas objects. 

3""" 

4 

5from __future__ import annotations 

6 

7from abc import ( 

8 ABC, 

9 abstractmethod, 

10) 

11from typing import ( 

12 TYPE_CHECKING, 

13 Any, 

14 Literal, 

15 Self, 

16 cast, 

17 final, 

18) 

19 

20import numpy as np 

21 

22from pandas._libs import ( 

23 NaT, 

24 lib, 

25) 

26from pandas._libs.tslibs import ( 

27 BaseOffset, 

28 Resolution, 

29 Tick, 

30 Timedelta, 

31 Timestamp, 

32 parsing, 

33 to_offset, 

34) 

35from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit 

36from pandas.compat.numpy import function as nv 

37from pandas.errors import ( 

38 InvalidIndexError, 

39 NullFrequencyError, 

40 OutOfBoundsDatetime, 

41 OutOfBoundsTimedelta, 

42) 

43from pandas.util._decorators import ( 

44 cache_readonly, 

45) 

46 

47from pandas.core.dtypes.common import ( 

48 is_integer, 

49 is_list_like, 

50) 

51from pandas.core.dtypes.concat import concat_compat 

52from pandas.core.dtypes.dtypes import ( 

53 CategoricalDtype, 

54 PeriodDtype, 

55) 

56 

57from pandas.core.arrays import ( 

58 DatetimeArray, 

59 ExtensionArray, 

60 PeriodArray, 

61 TimedeltaArray, 

62) 

63import pandas.core.common as com 

64import pandas.core.indexes.base as ibase 

65from pandas.core.indexes.base import ( 

66 Index, 

67) 

68from pandas.core.indexes.extension import NDArrayBackedExtensionIndex 

69from pandas.core.indexes.range import RangeIndex 

70from pandas.core.tools.timedeltas import to_timedelta 

71 

72if TYPE_CHECKING: 

73 from collections.abc import Sequence 

74 from datetime import datetime 

75 

76 from pandas._typing import ( 

77 Axis, 

78 JoinHow, 

79 TimeUnit, 

80 npt, 

81 ) 

82 

83 from pandas import CategoricalIndex 

84 

85_index_doc_kwargs = dict(ibase._index_doc_kwargs) 

86 

87 

88class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC): 

89 """ 

90 Common ops mixin to support a unified interface datetimelike Index. 

91 """ 

92 

93 _can_hold_strings = False 

94 _data: DatetimeArray | TimedeltaArray | PeriodArray 

95 

96 def mean(self, *, skipna: bool = True, axis: int | None = 0): 

97 """ 

98 Return the mean value of the Array. 

99 

100 Parameters 

101 ---------- 

102 skipna : bool, default True 

103 Whether to ignore any NaT elements. 

104 axis : int, optional, default 0 

105 Axis for the function to be applied on. 

106 

107 Returns 

108 ------- 

109 scalar 

110 Timestamp or Timedelta. 

111 

112 See Also 

113 -------- 

114 numpy.ndarray.mean : Returns the average of array elements along a given axis. 

115 Series.mean : Return the mean value in a Series. 

116 

117 Notes 

118 ----- 

119 mean is only defined for Datetime and Timedelta dtypes, not for Period. 

120 

121 Examples 

122 -------- 

123 For :class:`pandas.DatetimeIndex`: 

124 

125 >>> idx = pd.date_range("2001-01-01 00:00", periods=3) 

126 >>> idx 

127 DatetimeIndex(['2001-01-01', '2001-01-02', '2001-01-03'], 

128 dtype='datetime64[us]', freq='D') 

129 >>> idx.mean() 

130 Timestamp('2001-01-02 00:00:00') 

131 

132 For :class:`pandas.TimedeltaIndex`: 

133 

134 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D") 

135 >>> tdelta_idx 

136 TimedeltaIndex(['1 days', '2 days', '3 days'], 

137 dtype='timedelta64[s]', freq=None) 

138 >>> tdelta_idx.mean() 

139 Timedelta('2 days 00:00:00') 

140 """ 

141 return self._data.mean(skipna=skipna, axis=axis) 

142 

143 @property 

144 def freq(self) -> BaseOffset | None: 

145 """ 

146 Return the frequency object if it is set, otherwise None. 

147 

148 To learn more about the frequency strings, please see 

149 :ref:`this link<timeseries.offset_aliases>`. 

150 

151 See Also 

152 -------- 

153 DatetimeIndex.freq : Return the frequency object if it is set, otherwise None. 

154 PeriodIndex.freq : Return the frequency object if it is set, otherwise None. 

155 

156 Examples 

157 -------- 

158 >>> datetimeindex = pd.date_range( 

159 ... "2022-02-22 02:22:22", periods=10, tz="America/Chicago", freq="h" 

160 ... ) 

161 >>> datetimeindex 

162 DatetimeIndex(['2022-02-22 02:22:22-06:00', '2022-02-22 03:22:22-06:00', 

163 '2022-02-22 04:22:22-06:00', '2022-02-22 05:22:22-06:00', 

164 '2022-02-22 06:22:22-06:00', '2022-02-22 07:22:22-06:00', 

165 '2022-02-22 08:22:22-06:00', '2022-02-22 09:22:22-06:00', 

166 '2022-02-22 10:22:22-06:00', '2022-02-22 11:22:22-06:00'], 

167 dtype='datetime64[us, America/Chicago]', freq='h') 

168 >>> datetimeindex.freq 

169 <Hour> 

170 """ 

171 return self._data.freq 

172 

173 @freq.setter 

174 def freq(self, value) -> None: 

175 # error: Property "freq" defined in "PeriodArray" is read-only [misc] 

176 self._data.freq = value # type: ignore[misc] 

177 

178 @property 

179 def asi8(self) -> npt.NDArray[np.int64]: 

180 return self._data.asi8 

181 

182 @property 

183 def freqstr(self) -> str: 

184 """ 

185 Return the frequency object as a string if it's set, otherwise None. 

186 

187 See Also 

188 -------- 

189 DatetimeIndex.inferred_freq : Returns a string representing a frequency 

190 generated by infer_freq. 

191 

192 Examples 

193 -------- 

194 For DatetimeIndex: 

195 

196 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00"], freq="D") 

197 >>> idx.freqstr 

198 'D' 

199 

200 The frequency can be inferred if there are more than 2 points: 

201 

202 >>> idx = pd.DatetimeIndex( 

203 ... ["2018-01-01", "2018-01-03", "2018-01-05"], freq="infer" 

204 ... ) 

205 >>> idx.freqstr 

206 '2D' 

207 

208 For PeriodIndex: 

209 

210 >>> idx = pd.PeriodIndex(["2023-1", "2023-2", "2023-3"], freq="M") 

211 >>> idx.freqstr 

212 'M' 

213 """ 

214 from pandas import PeriodIndex 

215 

216 if self._data.freqstr is not None and isinstance( 

217 self._data, (PeriodArray, PeriodIndex) 

218 ): 

219 freq = PeriodDtype(self._data.freq)._freqstr 

220 return freq 

221 else: 

222 return self._data.freqstr # type: ignore[return-value] 

223 

224 @cache_readonly 

225 @abstractmethod 

226 def _resolution_obj(self) -> Resolution: ... 

227 

228 @cache_readonly 

229 def resolution(self) -> str: 

230 """ 

231 Returns day, hour, minute, second, millisecond or microsecond 

232 """ 

233 return self._data.resolution 

234 

235 # ------------------------------------------------------------------------ 

236 

237 @cache_readonly 

238 def hasnans(self) -> bool: 

239 return self._data._hasna 

240 

241 def equals(self, other: Any) -> bool: 

242 """ 

243 Determines if two Index objects contain the same elements. 

244 """ 

245 if self.is_(other): 

246 return True 

247 

248 if not isinstance(other, Index): 

249 return False 

250 elif other.dtype.kind in "iufc": 

251 return False 

252 elif not isinstance(other, type(self)): 

253 should_try = False 

254 inferable = self._data._infer_matches 

255 if other.dtype == object: 

256 should_try = other.inferred_type in inferable 

257 elif isinstance(other.dtype, CategoricalDtype): 

258 other = cast("CategoricalIndex", other) 

259 should_try = other.categories.inferred_type in inferable 

260 

261 if should_try: 

262 try: 

263 other = type(self)(other) 

264 except (ValueError, TypeError, OverflowError): 

265 # e.g. 

266 # ValueError -> cannot parse str entry, or OutOfBoundsDatetime 

267 # TypeError -> trying to convert IntervalIndex to DatetimeIndex 

268 # OverflowError -> Index([very_large_timedeltas]) 

269 return False 

270 

271 if type(self) != type(other): 

272 return False 

273 elif self.dtype == other.dtype: 

274 return np.array_equal(self.asi8, other.asi8) 

275 elif (self.dtype.kind == "M" and self.tz == other.tz) or self.dtype.kind == "m": # type: ignore[attr-defined] 

276 # different units, otherwise matching 

277 try: 

278 # TODO: do this at the EA level? 

279 left, right = self._data._ensure_matching_resos(other._data) # type: ignore[union-attr] 

280 except (OutOfBoundsDatetime, OutOfBoundsTimedelta): 

281 return False 

282 else: 

283 return np.array_equal(left.view("i8"), right.view("i8")) 

284 return False 

285 

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

287 """ 

288 Return a boolean indicating whether the provided key is in the index. 

289 

290 Parameters 

291 ---------- 

292 key : label 

293 The key to check if it is present in the index. 

294 

295 Returns 

296 ------- 

297 bool 

298 Whether the key search is in the index. 

299 

300 Raises 

301 ------ 

302 TypeError 

303 If the key is not hashable. 

304 

305 See Also 

306 -------- 

307 Index.isin : Returns an ndarray of boolean dtype indicating whether the 

308 list-like key is in the index. 

309 

310 Examples 

311 -------- 

312 >>> idx = pd.Index([1, 2, 3, 4]) 

313 >>> idx 

314 Index([1, 2, 3, 4], dtype='int64') 

315 >>> 2 in idx 

316 True 

317 >>> 6 in idx 

318 False 

319 """ 

320 hash(key) 

321 try: 

322 self.get_loc(key) 

323 except (KeyError, TypeError, ValueError, InvalidIndexError): 

324 return False 

325 return True 

326 

327 def _convert_tolerance(self, tolerance, target): 

328 tolerance = np.asarray(to_timedelta(tolerance).to_numpy()) 

329 return super()._convert_tolerance(tolerance, target) 

330 

331 # -------------------------------------------------------------------- 

332 # Rendering Methods 

333 _default_na_rep = "NaT" 

334 

335 def _format_with_header( 

336 self, *, header: list[str], na_rep: str, date_format: str | None = None 

337 ) -> list[str]: 

338 # TODO: not reached in tests 2023-10-11 

339 # matches base class except for whitespace padding and date_format 

340 return header + list( 

341 self._get_values_for_csv(na_rep=na_rep, date_format=date_format) 

342 ) 

343 

344 @property 

345 def _formatter_func(self): 

346 return self._data._formatter() 

347 

348 def _format_attrs(self): 

349 """ 

350 Return a list of tuples of the (attr,formatted_value). 

351 """ 

352 attrs = super()._format_attrs() 

353 for attrib in self._attributes: 

354 # iterating over _attributes prevents us from doing this for PeriodIndex 

355 if attrib == "freq": 

356 freq = self.freqstr 

357 if freq is not None: 

358 freq = repr(freq) # e.g. D -> 'D' 

359 attrs.append(("freq", freq)) 

360 return attrs 

361 

362 def _summary(self, name=None) -> str: 

363 """ 

364 Return a summarized representation. 

365 

366 Parameters 

367 ---------- 

368 name : str 

369 name to use in the summary representation 

370 

371 Returns 

372 ------- 

373 String with a summarized representation of the index 

374 """ 

375 result = super()._summary(name=name) 

376 if self.freq: 

377 result += f"\nFreq: {self.freqstr}" 

378 

379 return result 

380 

381 # -------------------------------------------------------------------- 

382 # Indexing Methods 

383 

384 @final 

385 def _can_partial_date_slice(self, reso: Resolution) -> bool: 

386 # e.g. test_getitem_setitem_periodindex 

387 # History of conversation GH#3452, GH#3931, GH#2369, GH#14826 

388 return reso > self._resolution_obj 

389 # NB: for DTI/PI, not TDI 

390 

391 def _parsed_string_to_bounds(self, reso: Resolution, parsed): 

392 raise NotImplementedError 

393 

394 def _parse_with_reso(self, label: str) -> tuple[datetime, Resolution]: 

395 # overridden by TimedeltaIndex 

396 try: 

397 if self.freq is None or hasattr(self.freq, "rule_code"): 

398 freq = self.freq 

399 except NotImplementedError: 

400 freq = getattr(self, "freqstr", getattr(self, "inferred_freq", None)) 

401 

402 freqstr: str | None 

403 if freq is not None and not isinstance(freq, str): 

404 freqstr = freq.rule_code 

405 else: 

406 freqstr = freq 

407 

408 if isinstance(label, np.str_): 

409 # GH#45580 

410 label = str(label) 

411 

412 parsed, reso_str = parsing.parse_datetime_string_with_reso(label, freqstr) 

413 reso = Resolution.from_attrname(reso_str) 

414 return parsed, reso 

415 

416 def _get_string_slice(self, key: str) -> slice | npt.NDArray[np.intp]: 

417 # overridden by TimedeltaIndex 

418 parsed, reso = self._parse_with_reso(key) 

419 try: 

420 return self._partial_date_slice(reso, parsed) 

421 except KeyError as err: 

422 raise KeyError(key) from err 

423 

424 @final 

425 def _partial_date_slice( 

426 self, 

427 reso: Resolution, 

428 parsed: datetime, 

429 ) -> slice | npt.NDArray[np.intp]: 

430 """ 

431 Parameters 

432 ---------- 

433 reso : Resolution 

434 parsed : datetime 

435 

436 Returns 

437 ------- 

438 slice or ndarray[intp] 

439 """ 

440 if not self._can_partial_date_slice(reso): 

441 raise ValueError 

442 

443 t1, t2 = self._parsed_string_to_bounds(reso, parsed) 

444 vals = self._data._ndarray 

445 unbox = self._data._unbox 

446 

447 if self.is_monotonic_increasing: 

448 if len(self) and ( 

449 (t1 < self[0] and t2 < self[0]) or (t1 > self[-1] and t2 > self[-1]) 

450 ): 

451 # we are out of range 

452 raise KeyError 

453 

454 # TODO: does this depend on being monotonic _increasing_? 

455 

456 # a monotonic (sorted) series can be sliced 

457 left = vals.searchsorted(unbox(t1), side="left") 

458 right = vals.searchsorted(unbox(t2), side="right") 

459 return slice(left, right) 

460 

461 else: 

462 lhs_mask = vals >= unbox(t1) 

463 rhs_mask = vals <= unbox(t2) 

464 

465 # try to find the dates 

466 return (lhs_mask & rhs_mask).nonzero()[0] 

467 

468 def _maybe_cast_slice_bound(self, label, side: str): 

469 """ 

470 If label is a string, cast it to scalar type according to resolution. 

471 

472 Parameters 

473 ---------- 

474 label : object 

475 side : {'left', 'right'} 

476 

477 Returns 

478 ------- 

479 label : object 

480 

481 Notes 

482 ----- 

483 Value of `side` parameter should be validated in caller. 

484 """ 

485 if isinstance(label, str): 

486 try: 

487 parsed, reso = self._parse_with_reso(label) 

488 except ValueError as err: 

489 # DTI -> parsing.DateParseError 

490 # TDI -> 'unit abbreviation w/o a number' 

491 # PI -> string cannot be parsed as datetime-like 

492 self._raise_invalid_indexer("slice", label, err) 

493 

494 lower, upper = self._parsed_string_to_bounds(reso, parsed) 

495 return lower if side == "left" else upper 

496 elif not isinstance(label, self._data._recognized_scalars): 

497 self._raise_invalid_indexer("slice", label) 

498 

499 return label 

500 

501 # -------------------------------------------------------------------- 

502 # Arithmetic Methods 

503 

504 def shift(self, periods: int = 1, freq=None) -> Self: 

505 """ 

506 Shift index by desired number of time frequency increments. 

507 

508 This method is for shifting the values of datetime-like indexes 

509 by a specified time increment a given number of times. 

510 

511 Parameters 

512 ---------- 

513 periods : int, default 1 

514 Number of periods (or increments) to shift by, 

515 can be positive or negative. 

516 freq : pandas.DateOffset, pandas.Timedelta or string, optional 

517 Frequency increment to shift by. 

518 If None, the index is shifted by its own `freq` attribute. 

519 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc. 

520 

521 Returns 

522 ------- 

523 pandas.DatetimeIndex 

524 Shifted index. 

525 

526 See Also 

527 -------- 

528 Index.shift : Shift values of Index. 

529 PeriodIndex.shift : Shift values of PeriodIndex. 

530 """ 

531 raise NotImplementedError 

532 

533 # -------------------------------------------------------------------- 

534 

535 def _maybe_cast_listlike_indexer(self, keyarr): 

536 """ 

537 Analogue to maybe_cast_indexer for get_indexer instead of get_loc. 

538 """ 

539 try: 

540 res = self._data._validate_listlike(keyarr, allow_object=True) 

541 except (ValueError, TypeError): 

542 if not isinstance(keyarr, ExtensionArray): 

543 # e.g. we don't want to cast DTA to ndarray[object] 

544 res = com.asarray_tuplesafe(keyarr) 

545 # TODO: com.asarray_tuplesafe shouldn't cast e.g. DatetimeArray 

546 else: 

547 res = keyarr 

548 return Index(res, dtype=res.dtype) 

549 

550 

551class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC): 

552 """ 

553 Mixin class for methods shared by DatetimeIndex and TimedeltaIndex, 

554 but not PeriodIndex 

555 """ 

556 

557 _data: DatetimeArray | TimedeltaArray 

558 _comparables = ["name", "freq"] 

559 _attributes = ["name", "freq"] 

560 

561 # Compat for frequency inference, see GH#23789 

562 _is_monotonic_increasing = Index.is_monotonic_increasing 

563 _is_monotonic_decreasing = Index.is_monotonic_decreasing 

564 _is_unique = Index.is_unique 

565 

566 @property 

567 def unit(self) -> TimeUnit: 

568 return self._data.unit 

569 

570 def as_unit(self, unit: TimeUnit) -> Self: 

571 """ 

572 Convert to a dtype with the given unit resolution. 

573 

574 This method is for converting the dtype of a ``DatetimeIndex`` or 

575 ``TimedeltaIndex`` to a new dtype with the given unit 

576 resolution/precision. 

577 

578 Parameters 

579 ---------- 

580 unit : {'s', 'ms', 'us', 'ns'} 

581 

582 Returns 

583 ------- 

584 same type as self 

585 Converted to the specified unit. 

586 

587 See Also 

588 -------- 

589 Timestamp.as_unit : Convert to the given unit. 

590 Timedelta.as_unit : Convert to the given unit. 

591 DatetimeIndex.as_unit : Convert to the given unit. 

592 TimedeltaIndex.as_unit : Convert to the given unit. 

593 

594 Examples 

595 -------- 

596 For :class:`pandas.DatetimeIndex`: 

597 

598 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"]) 

599 >>> idx 

600 DatetimeIndex(['2020-01-02 01:02:03.004005006'], 

601 dtype='datetime64[ns]', freq=None) 

602 >>> idx.as_unit("s") 

603 DatetimeIndex(['2020-01-02 01:02:03'], dtype='datetime64[s]', freq=None) 

604 

605 For :class:`pandas.TimedeltaIndex`: 

606 

607 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"]) 

608 >>> tdelta_idx 

609 TimedeltaIndex(['1 days 00:03:00.000002042'], 

610 dtype='timedelta64[ns]', freq=None) 

611 >>> tdelta_idx.as_unit("s") 

612 TimedeltaIndex(['1 days 00:03:00'], dtype='timedelta64[s]', freq=None) 

613 """ 

614 arr = self._data.as_unit(unit) 

615 return type(self)._simple_new(arr, name=self.name) 

616 

617 def _with_freq(self, freq): 

618 arr = self._data._with_freq(freq) 

619 return type(self)._simple_new(arr, name=self._name) 

620 

621 @property 

622 def values(self) -> np.ndarray: 

623 # NB: For Datetime64TZ this is lossy 

624 data = self._data._ndarray 

625 data = data.view() 

626 data.flags.writeable = False 

627 return data 

628 

629 def shift(self, periods: int = 1, freq=None) -> Self: 

630 """ 

631 Shift index by desired number of time frequency increments. 

632 This method is for shifting the values of datetime-like indexes 

633 by a specified time increment a given number of times. 

634 

635 Parameters 

636 ---------- 

637 periods : int, default 1 

638 Number of periods (or increments) to shift by, 

639 can be positive or negative. 

640 freq : pandas.DateOffset, pandas.Timedelta or string, optional 

641 Frequency increment to shift by. 

642 If None, the index is shifted by its own `freq` attribute. 

643 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc. 

644 

645 Returns 

646 ------- 

647 pandas.DatetimeIndex 

648 Shifted index. 

649 

650 See Also 

651 -------- 

652 Index.shift : Shift values of Index. 

653 PeriodIndex.shift : Shift values of PeriodIndex. 

654 """ 

655 if freq is not None and freq != self.freq: 

656 if isinstance(freq, str): 

657 freq = to_offset(freq) 

658 offset = periods * freq 

659 return self + offset 

660 

661 if periods == 0 or len(self) == 0: 

662 # GH#14811 empty case 

663 return self.copy() 

664 

665 if self.freq is None: 

666 raise NullFrequencyError("Cannot shift with no freq") 

667 

668 start = self[0] + periods * self.freq 

669 end = self[-1] + periods * self.freq 

670 

671 # Note: in the DatetimeTZ case, _generate_range will infer the 

672 # appropriate timezone from `start` and `end`, so tz does not need 

673 # to be passed explicitly. 

674 result = self._data._generate_range( 

675 start=start, end=end, periods=None, freq=self.freq, unit=self.unit 

676 ) 

677 return type(self)._simple_new(result, name=self.name) 

678 

679 @cache_readonly 

680 def inferred_freq(self) -> str | None: 

681 """ 

682 Return the inferred frequency of the index. 

683 

684 Returns 

685 ------- 

686 str or None 

687 A string representing a frequency generated by ``infer_freq``. 

688 Returns ``None`` if the frequency cannot be inferred. 

689 

690 See Also 

691 -------- 

692 DatetimeIndex.freqstr : Return the frequency object as a string if it's set, 

693 otherwise ``None``. 

694 

695 Examples 

696 -------- 

697 For ``DatetimeIndex``: 

698 

699 >>> idx = pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"]) 

700 >>> idx.inferred_freq 

701 '2D' 

702 

703 For ``TimedeltaIndex``: 

704 

705 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"]) 

706 >>> tdelta_idx 

707 TimedeltaIndex(['0 days', '10 days', '20 days'], 

708 dtype='timedelta64[us]', freq=None) 

709 >>> tdelta_idx.inferred_freq 

710 '10D' 

711 """ 

712 return self._data.inferred_freq 

713 

714 # -------------------------------------------------------------------- 

715 # Set Operation Methods 

716 

717 @cache_readonly 

718 def _as_range_index(self) -> RangeIndex: 

719 # Convert our i8 representations to RangeIndex 

720 # Caller is responsible for checking isinstance(self.freq, Tick) 

721 freq = cast(Tick, self.freq) 

722 tick = Timedelta(freq).as_unit(self.unit)._value 

723 rng = range(self[0]._value, self[-1]._value + tick, tick) 

724 return RangeIndex(rng) 

725 

726 def _can_range_setop(self, other) -> bool: 

727 return isinstance(self.freq, Tick) and isinstance(other.freq, Tick) 

728 

729 def _wrap_range_setop(self, other, res_i8) -> Self: 

730 new_freq = None 

731 if not len(res_i8): 

732 # RangeIndex defaults to step=1, which we don't want. 

733 new_freq = self.freq 

734 elif isinstance(res_i8, RangeIndex): 

735 new_freq = to_offset( 

736 Timedelta(res_i8.step, unit=self.unit).as_unit(self.unit) 

737 ) 

738 

739 # TODO(GH#41493): we cannot just do 

740 # type(self._data)(res_i8.values, dtype=self.dtype, freq=new_freq) 

741 # because test_setops_preserve_freq fails with _validate_frequency raising. 

742 # This raising is incorrect, as 'on_freq' is incorrect. This will 

743 # be fixed by GH#41493 

744 res_values = res_i8.values.view(self._data._ndarray.dtype) 

745 result = type(self._data)._simple_new( 

746 # error: Argument "dtype" to "_simple_new" of "DatetimeArray" has 

747 # incompatible type "Union[dtype[Any], ExtensionDtype]"; expected 

748 # "Union[dtype[datetime64], DatetimeTZDtype]" 

749 res_values, 

750 dtype=self.dtype, # type: ignore[arg-type] 

751 freq=new_freq, # type: ignore[arg-type] 

752 ) 

753 return cast("Self", self._wrap_setop_result(other, result)) 

754 

755 def _range_intersect(self, other, sort) -> Self: 

756 # Dispatch to RangeIndex intersection logic. 

757 left = self._as_range_index 

758 right = other._as_range_index 

759 res_i8 = left.intersection(right, sort=sort) 

760 return self._wrap_range_setop(other, res_i8) 

761 

762 def _range_union(self, other, sort) -> Self: 

763 # Dispatch to RangeIndex union logic. 

764 left = self._as_range_index 

765 right = other._as_range_index 

766 res_i8 = left.union(right, sort=sort) 

767 return self._wrap_range_setop(other, res_i8) 

768 

769 def _intersection(self, other: Index, sort: bool = False) -> Index: 

770 """ 

771 intersection specialized to the case with matching dtypes and both non-empty. 

772 """ 

773 other = cast("DatetimeTimedeltaMixin", other) 

774 

775 if self._can_range_setop(other): 

776 return self._range_intersect(other, sort=sort) 

777 

778 if not self._can_fast_intersect(other): 

779 result = Index._intersection(self, other, sort=sort) 

780 # We need to invalidate the freq because Index._intersection 

781 # uses _shallow_copy on a view of self._data, which will preserve 

782 # self.freq if we're not careful. 

783 # At this point we should have result.dtype == self.dtype 

784 # and type(result) is type(self._data) 

785 result = self._wrap_setop_result(other, result) 

786 # error: "Index" has no attribute "_with_freq"; maybe "_with_infer"? 

787 return result._with_freq(None)._with_freq("infer") # type: ignore[attr-defined] 

788 

789 else: 

790 return self._fast_intersect(other, sort) 

791 

792 def _fast_intersect(self, other, sort): 

793 # to make our life easier, "sort" the two ranges 

794 if self[0] <= other[0]: 

795 left, right = self, other 

796 else: 

797 left, right = other, self 

798 

799 # after sorting, the intersection always starts with the right index 

800 # and ends with the index of which the last elements is smallest 

801 end = min(left[-1], right[-1]) 

802 start = right[0] 

803 

804 if end < start: 

805 result = self[:0] 

806 else: 

807 lslice = slice(*left.slice_locs(start, end)) 

808 result = left._values[lslice] 

809 

810 return result 

811 

812 def _can_fast_intersect(self, other: Self) -> bool: 

813 # Note: we only get here with len(self) > 0 and len(other) > 0 

814 if self.freq is None: 

815 return False 

816 

817 elif other.freq != self.freq: 

818 return False 

819 

820 elif not self.is_monotonic_increasing: 

821 # Because freq is not None, we must then be monotonic decreasing 

822 return False 

823 

824 # this along with matching freqs ensure that we "line up", 

825 # so intersection will preserve freq 

826 # Note we are assuming away Ticks, as those go through _range_intersect 

827 # GH#42104 

828 return self.freq.n == 1 

829 

830 def _can_fast_union(self, other: Self) -> bool: 

831 # Assumes that type(self) == type(other), as per the annotation 

832 # The ability to fast_union also implies that `freq` should be 

833 # retained on union. 

834 freq = self.freq 

835 

836 if freq is None or freq != other.freq: 

837 return False 

838 

839 if not self.is_monotonic_increasing: 

840 # Because freq is not None, we must then be monotonic decreasing 

841 # TODO: do union on the reversed indexes? 

842 return False 

843 

844 if len(self) == 0 or len(other) == 0: 

845 # only reached via union_many 

846 return True 

847 

848 # to make our life easier, "sort" the two ranges 

849 if self[0] <= other[0]: 

850 left, right = self, other 

851 else: 

852 left, right = other, self 

853 

854 right_start = right[0] 

855 left_end = left[-1] 

856 

857 # Only need to "adjoin", not overlap 

858 return (right_start == left_end + freq) or right_start in left 

859 

860 def _fast_union(self, other: Self, sort=None) -> Self: 

861 # Caller is responsible for ensuring self and other are non-empty 

862 

863 # to make our life easier, "sort" the two ranges 

864 if self[0] <= other[0]: 

865 left, right = self, other 

866 elif sort is False: 

867 # TDIs are not in the "correct" order and we don't want 

868 # to sort but want to remove overlaps 

869 left, right = self, other 

870 left_start = left[0] 

871 loc = right.searchsorted(left_start, side="left") 

872 right_chunk = right._values[:loc] 

873 dates = concat_compat((left._values, right_chunk)) 

874 result = type(self)._simple_new(dates, name=self.name) 

875 return result 

876 else: 

877 left, right = other, self 

878 

879 left_end = left[-1] 

880 right_end = right[-1] 

881 

882 # concatenate 

883 if left_end < right_end: 

884 loc = right.searchsorted(left_end, side="right") 

885 right_chunk = right._values[loc:] 

886 dates = concat_compat([left._values, right_chunk]) 

887 # The can_fast_union check ensures that the result.freq 

888 # should match self.freq 

889 assert isinstance(dates, type(self._data)) 

890 # error: Item "ExtensionArray" of "ExtensionArray | 

891 # ndarray[Any, Any]" has no attribute "_freq" 

892 assert dates._freq == self.freq # type: ignore[union-attr] 

893 result = type(self)._simple_new(dates) 

894 return result 

895 else: 

896 return left 

897 

898 def _union(self, other, sort): 

899 # We are called by `union`, which is responsible for this validation 

900 assert isinstance(other, type(self)) 

901 assert self.dtype == other.dtype 

902 

903 if self._can_range_setop(other): 

904 return self._range_union(other, sort=sort) 

905 

906 if self._can_fast_union(other): 

907 result = self._fast_union(other, sort=sort) 

908 # in the case with sort=None, the _can_fast_union check ensures 

909 # that result.freq == self.freq 

910 return result 

911 else: 

912 return super()._union(other, sort)._with_freq("infer") 

913 

914 # -------------------------------------------------------------------- 

915 # Join Methods 

916 

917 def _get_join_freq(self, other): 

918 """ 

919 Get the freq to attach to the result of a join operation. 

920 """ 

921 freq = None 

922 if self._can_fast_union(other): 

923 freq = self.freq 

924 return freq 

925 

926 def _wrap_join_result( 

927 self, 

928 joined, 

929 other, 

930 lidx: npt.NDArray[np.intp] | None, 

931 ridx: npt.NDArray[np.intp] | None, 

932 how: JoinHow, 

933 ) -> tuple[Self, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]: 

934 assert other.dtype == self.dtype, (other.dtype, self.dtype) 

935 join_index, lidx, ridx = super()._wrap_join_result( 

936 joined, other, lidx, ridx, how 

937 ) 

938 join_index._data._freq = self._get_join_freq(other) 

939 return join_index, lidx, ridx 

940 

941 def _get_engine_target(self) -> np.ndarray: 

942 # engine methods and libjoin methods need dt64/td64 values cast to i8 

943 return self._data._ndarray.view("i8") 

944 

945 def _from_join_target(self, result: np.ndarray): 

946 # view e.g. i8 back to M8[ns] 

947 result = result.view(self._data._ndarray.dtype) 

948 return self._data._from_backing_data(result) 

949 

950 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"): 

951 if ( 

952 self.is_monotonic_increasing 

953 and isinstance(label, (Timestamp, Timedelta)) 

954 and abbrev_to_npy_unit(label.unit) > abbrev_to_npy_unit(self.unit) 

955 ): 

956 # For non-matching units we can safely round down (with side=right) 

957 # This is needed for GH#63262 

958 if side == "right": 

959 label = label.as_unit(self.unit) # this should always be a round-down 

960 else: 

961 # round up 

962 label = label.ceil(self.unit).as_unit(self.unit) 

963 

964 return super()._searchsorted_monotonic(label, side) 

965 

966 # -------------------------------------------------------------------- 

967 # List-like Methods 

968 

969 def _get_delete_freq(self, loc: int | slice | Sequence[int]): 

970 """ 

971 Find the `freq` for self.delete(loc). 

972 """ 

973 freq = None 

974 if self.freq is not None: 

975 if is_integer(loc): 

976 if loc in (0, -len(self), -1, len(self) - 1): 

977 freq = self.freq 

978 else: 

979 if is_list_like(loc): 

980 # error: Incompatible types in assignment (expression has 

981 # type "Union[slice, ndarray]", variable has type 

982 # "Union[int, slice, Sequence[int]]") 

983 loc = lib.maybe_indices_to_slice( # type: ignore[assignment] 

984 np.asarray(loc, dtype=np.intp), len(self) 

985 ) 

986 if isinstance(loc, slice) and loc.step in (1, None): 

987 if loc.start in (0, None) or loc.stop in (len(self), None): 

988 freq = self.freq 

989 return freq 

990 

991 def _get_insert_freq(self, loc: int, item): 

992 """ 

993 Find the `freq` for self.insert(loc, item). 

994 """ 

995 value = self._data._validate_scalar(item) 

996 item = self._data._box_func(value) 

997 

998 freq = None 

999 if self.freq is not None: 

1000 # freq can be preserved on edge cases 

1001 if self.size: 

1002 if item is NaT: 

1003 pass 

1004 elif loc in (0, -len(self)) and item + self.freq == self[0]: 

1005 freq = self.freq 

1006 elif (loc == len(self)) and item - self.freq == self[-1]: 

1007 freq = self.freq 

1008 # Adding a single item to an empty index may preserve freq 

1009 elif isinstance(self.freq, Tick): 

1010 # all TimedeltaIndex cases go through here; is_on_offset 

1011 # would raise TypeError 

1012 freq = self.freq 

1013 elif self.freq.is_on_offset(item): 

1014 freq = self.freq 

1015 return freq 

1016 

1017 def delete(self, loc) -> Self: 

1018 """ 

1019 Make new Index with passed location(-s) deleted. 

1020 

1021 Parameters 

1022 ---------- 

1023 loc : int or list of int 

1024 Location of item(-s) which will be deleted. 

1025 Use a list of locations to delete more than one value at the same time. 

1026 

1027 Returns 

1028 ------- 

1029 Index 

1030 Will be same type as self, except for RangeIndex. 

1031 

1032 See Also 

1033 -------- 

1034 numpy.delete : Delete any rows and column from NumPy array (ndarray). 

1035 

1036 Examples 

1037 -------- 

1038 >>> idx = pd.Index(["a", "b", "c"]) 

1039 >>> idx.delete(1) 

1040 Index(['a', 'c'], dtype='str') 

1041 >>> idx = pd.Index(["a", "b", "c"]) 

1042 >>> idx.delete([0, 2]) 

1043 Index(['b'], dtype='str') 

1044 """ 

1045 result = super().delete(loc) 

1046 result._data._freq = self._get_delete_freq(loc) 

1047 return result 

1048 

1049 def insert(self, loc: int, item): 

1050 """ 

1051 Make new Index inserting new item at location. 

1052 Follows Python numpy.insert semantics for negative values. 

1053 

1054 Parameters 

1055 ---------- 

1056 loc : int 

1057 The integer location where the new item will be inserted. 

1058 item : object 

1059 The new item to be inserted into the Index. 

1060 

1061 Returns 

1062 ------- 

1063 Index 

1064 Returns a new Index object resulting from inserting the specified item at 

1065 the specified location within the original Index. 

1066 

1067 See Also 

1068 -------- 

1069 Index.append : Append a collection of Indexes together. 

1070 

1071 Examples 

1072 -------- 

1073 >>> idx = pd.Index(["a", "b", "c"]) 

1074 >>> idx.insert(1, "x") 

1075 Index(['a', 'x', 'b', 'c'], dtype='str') 

1076 """ 

1077 result = super().insert(loc, item) 

1078 if isinstance(result, type(self)): 

1079 # i.e. parent class method did not cast 

1080 result._data._freq = self._get_insert_freq(loc, item) 

1081 return result 

1082 

1083 # -------------------------------------------------------------------- 

1084 # NDArray-Like Methods 

1085 

1086 def take( 

1087 self, 

1088 indices, 

1089 axis: Axis = 0, 

1090 allow_fill: bool = True, 

1091 fill_value=None, 

1092 **kwargs, 

1093 ) -> Self: 

1094 """ 

1095 Return a new Index of the values selected by the indices. 

1096 For internal compatibility with numpy arrays. 

1097 

1098 Parameters 

1099 ---------- 

1100 indices : array-like 

1101 Indices to be taken. 

1102 axis : {0 or 'index'}, optional 

1103 The axis over which to select values, always 0 or 'index'. 

1104 allow_fill : bool, default True 

1105 How to handle negative values in `indices`. 

1106 * False: negative values in `indices` indicate positional indices 

1107 from the right (the default). This is similar to 

1108 :func:`numpy.take`. 

1109 * True: negative values in `indices` indicate 

1110 missing values. These values are set to `fill_value`. Any other 

1111 other negative values raise a ``ValueError``. 

1112 fill_value : scalar, default None 

1113 If allow_fill=True and fill_value is not None, indices specified by 

1114 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError. 

1115 **kwargs 

1116 Required for compatibility with numpy. 

1117 

1118 Returns 

1119 ------- 

1120 Index 

1121 An index formed of elements at the given indices. Will be the same 

1122 type as self, except for RangeIndex. 

1123 

1124 See Also 

1125 -------- 

1126 numpy.ndarray.take: Return an array formed from the 

1127 elements of a at the given indices. 

1128 

1129 Examples 

1130 -------- 

1131 >>> idx = pd.Index(["a", "b", "c"]) 

1132 >>> idx.take([2, 2, 1, 2]) 

1133 Index(['c', 'c', 'b', 'c'], dtype='str') 

1134 """ 

1135 nv.validate_take((), kwargs) 

1136 indices = np.asarray(indices, dtype=np.intp) 

1137 

1138 result = NDArrayBackedExtensionIndex.take( 

1139 self, indices, axis, allow_fill, fill_value, **kwargs 

1140 ) 

1141 

1142 maybe_slice = lib.maybe_indices_to_slice(indices, len(self)) 

1143 if isinstance(maybe_slice, slice): 

1144 freq = self._data._get_getitem_freq(maybe_slice) 

1145 result._data._freq = freq 

1146 return result