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

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

606 statements  

1""" 

2Base and utility classes for tseries type pandas objects. 

3""" 

4 

5from __future__ import annotations 

6 

7from abc import ( 

8 ABC, 

9) 

10from itertools import pairwise 

11import operator 

12from typing import ( 

13 TYPE_CHECKING, 

14 Any, 

15 Literal, 

16 Self, 

17 cast, 

18 final, 

19) 

20import warnings 

21 

22import numpy as np 

23 

24from pandas._libs import ( 

25 NaT, 

26 lib, 

27) 

28from pandas._libs.tslibs import ( 

29 BaseOffset, 

30 Day, 

31 Resolution, 

32 Tick, 

33 Timedelta, 

34 Timestamp, 

35 get_resolution, 

36 parsing, 

37 timezones, 

38 to_offset, 

39) 

40from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit 

41from pandas._libs.tslibs.offsets import ( 

42 RelativeDeltaOffset, 

43 Week, 

44) 

45from pandas.compat.numpy import function as nv 

46from pandas.errors import ( 

47 InvalidIndexError, 

48 NullFrequencyError, 

49 OutOfBoundsDatetime, 

50 OutOfBoundsTimedelta, 

51 Pandas4Warning, 

52) 

53from pandas.util._decorators import ( 

54 cache_readonly, 

55) 

56from pandas.util._exceptions import find_stack_level 

57 

58from pandas.core.dtypes.common import ( 

59 is_integer, 

60 is_list_like, 

61) 

62from pandas.core.dtypes.concat import concat_compat 

63from pandas.core.dtypes.dtypes import ( 

64 CategoricalDtype, 

65 DatetimeTZDtype, 

66 PeriodDtype, 

67) 

68 

69from pandas.core import ( 

70 algorithms, 

71 roperator, 

72) 

73from pandas.core.arrays import ( 

74 DatetimeArray, 

75 ExtensionArray, 

76 PeriodArray, 

77 TimedeltaArray, 

78) 

79import pandas.core.common as com 

80from pandas.core.construction import ensure_wrapped_if_datetimelike 

81from pandas.core.indexers import check_array_indexer 

82from pandas.core.indexes.base import ( 

83 Index, 

84) 

85from pandas.core.indexes.extension import NDArrayBackedExtensionIndex 

86from pandas.core.indexes.range import RangeIndex 

87from pandas.core.tools.timedeltas import to_timedelta 

88 

89from pandas.tseries import frequencies 

90 

91if TYPE_CHECKING: 

92 from collections.abc import ( 

93 Hashable, 

94 Sequence, 

95 ) 

96 from datetime import datetime 

97 

98 from pandas._typing import ( 

99 Axis, 

100 JoinHow, 

101 TimeUnit, 

102 npt, 

103 ) 

104 

105 from pandas import CategoricalIndex 

106 

107 

108class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC): 

109 """ 

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

111 """ 

112 

113 _can_hold_strings = False 

114 _data: DatetimeArray | TimedeltaArray | PeriodArray 

115 _warn_quarter: bool = True 

116 

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

118 """ 

119 Return the mean value of the Array. 

120 

121 This method computes the arithmetic mean of the datetime or timedelta 

122 values in the index, optionally skipping NaT values. 

123 

124 Parameters 

125 ---------- 

126 skipna : bool, default True 

127 Whether to ignore any NaT elements. 

128 axis : int, optional, default 0 

129 Axis for the function to be applied on. 

130 

131 Returns 

132 ------- 

133 scalar 

134 Timestamp or Timedelta. 

135 

136 See Also 

137 -------- 

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

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

140 

141 Notes 

142 ----- 

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

144 

145 Examples 

146 -------- 

147 For :class:`pandas.DatetimeIndex`: 

148 

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

150 >>> idx 

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

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

153 >>> idx.mean() 

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

155 

156 For :class:`pandas.TimedeltaIndex`: 

157 

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

159 >>> tdelta_idx 

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

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

162 >>> tdelta_idx.mean() 

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

164 """ 

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

166 

167 @property 

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

169 """ 

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

171 

172 To learn more about the frequency strings, please see 

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

174 

175 See Also 

176 -------- 

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

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

179 

180 Examples 

181 -------- 

182 >>> datetimeindex = pd.date_range( 

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

184 ... ) 

185 >>> datetimeindex 

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

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

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

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

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

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

192 >>> datetimeindex.freq 

193 <Hour> 

194 """ 

195 # PeriodIndex reads from the array (derived from dtype); 

196 # DatetimeTimedeltaMixin overrides to read from self._freq. 

197 return self._data.freq # type: ignore[union-attr] 

198 

199 @property 

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

201 """ 

202 Return Integer representation of the values. 

203 

204 For :class:`DatetimeIndex` and :class:`TimedeltaIndex`, the 

205 values are the number of time units (determined by the index 

206 resolution) since the epoch. For :class:`PeriodIndex`, the 

207 values are ordinals. 

208 

209 Returns 

210 ------- 

211 numpy.ndarray 

212 An ndarray with int64 dtype. 

213 

214 See Also 

215 -------- 

216 Index.values : Return an array representing the data in the Index, 

217 using native types (datetime64, timedelta64) rather than int64. 

218 Index.to_numpy : Return a NumPy ndarray of the index values. 

219 

220 Examples 

221 -------- 

222 For :class:`DatetimeIndex` with default microsecond resolution: 

223 

224 >>> idx = pd.DatetimeIndex(["2023-01-01", "2023-01-02"], dtype="datetime64[us]") 

225 >>> idx.asi8 

226 array([1672531200000000, 1672617600000000]) 

227 

228 For :class:`TimedeltaIndex` with millisecond resolution: 

229 

230 >>> idx = pd.TimedeltaIndex(["1 day", "2 days"], dtype="timedelta64[ms]") 

231 >>> idx.asi8 

232 array([ 86400000, 172800000]) 

233 

234 For :class:`PeriodIndex`: 

235 

236 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M") 

237 >>> idx.asi8 

238 array([636, 637, 638]) 

239 """ 

240 return self._data.asi8 

241 

242 @property 

243 def freqstr(self) -> str: 

244 """ 

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

246 

247 This property returns a string representation of the frequency 

248 (e.g., ``'D'`` for daily, ``'h'`` for hourly) when one has been set 

249 on the index, either explicitly or via inference. 

250 

251 See Also 

252 -------- 

253 DatetimeIndex.inferred_freq : Returns a string representing a frequency 

254 generated by infer_freq. 

255 

256 Examples 

257 -------- 

258 For DatetimeIndex: 

259 

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

261 >>> idx.freqstr 

262 'D' 

263 

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

265 

266 >>> idx = pd.DatetimeIndex( 

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

268 ... ) 

269 >>> idx.freqstr 

270 '2D' 

271 

272 For PeriodIndex: 

273 

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

275 >>> idx.freqstr 

276 'M' 

277 """ 

278 freq = self.freq 

279 if freq is None: 

280 return freq # type: ignore[return-value] 

281 if isinstance(self._data, PeriodArray): 

282 return PeriodDtype(freq)._freqstr 

283 return freq.freqstr 

284 

285 @cache_readonly 

286 def _resolution_obj(self) -> Resolution: 

287 if isinstance(self.dtype, PeriodDtype): 

288 return self.dtype._resolution_obj 

289 elif self.dtype.kind == "M": 

290 return get_resolution(self.asi8, self.tz, reso=self._data._creso) # type: ignore[attr-defined,union-attr] 

291 else: 

292 return get_resolution(self.asi8, tz=None, reso=self._data._creso) # type: ignore[union-attr] 

293 

294 @cache_readonly 

295 def resolution(self) -> str: 

296 """ 

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

298 """ 

299 return self._resolution_obj.attrname 

300 

301 # ------------------------------------------------------------------------ 

302 

303 @cache_readonly 

304 def hasnans(self) -> bool: 

305 return self._data._hasna 

306 

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

308 """ 

309 Determines if two Index objects contain the same elements. 

310 """ 

311 if self.is_(other): 

312 return True 

313 

314 if not isinstance(other, Index): 

315 return False 

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

317 return False 

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

319 if other.dtype == object: 

320 # Use the cached inferred_type as a cheap pre-filter 

321 inferred = other.inferred_type 

322 compatible: tuple[str, ...] 

323 if self.dtype.kind == "M": 

324 compatible = ("datetime", "datetime64", "date") 

325 elif self.dtype.kind == "m": 

326 compatible = ("timedelta", "timedelta64") 

327 else: 

328 compatible = ("period",) 

329 if inferred not in compatible: 

330 return False 

331 converted = lib.maybe_convert_objects( 

332 np.asarray(other), convert_non_numeric=True 

333 ) 

334 converted = ensure_wrapped_if_datetimelike(converted) 

335 if isinstance(converted, type(self._data)): 

336 other = type(self)._simple_new(converted) 

337 elif self.dtype.kind == "M" and inferred == "date": 

338 # GH#65056 

339 warnings.warn( 

340 "Inferring datetime64 from data containing " 

341 "datetime.date objects is deprecated. In a future " 

342 "version, these will be left as object dtype. Use " 

343 "pd.to_datetime to explicitly convert to datetime64.", 

344 Pandas4Warning, 

345 stacklevel=find_stack_level(), 

346 ) 

347 try: 

348 other = type(self)(other) 

349 except (ValueError, TypeError, OverflowError): 

350 return False 

351 elif isinstance(other.dtype, CategoricalDtype): 

352 other = cast("CategoricalIndex", other) 

353 cat_vals = np.asarray(other.categories) 

354 if cat_vals.dtype == object: 

355 converted = lib.maybe_convert_objects( 

356 cat_vals, convert_non_numeric=True 

357 ) 

358 converted = ensure_wrapped_if_datetimelike(converted) 

359 else: 

360 converted = ensure_wrapped_if_datetimelike(cat_vals) 

361 if isinstance(converted, type(self._data)): 

362 try: 

363 other = type(self)(other) 

364 except (ValueError, TypeError, OverflowError): 

365 return False 

366 elif ( 

367 self.dtype.kind == "M" 

368 and cat_vals.dtype == object 

369 and lib.infer_dtype(other.categories) == "date" 

370 ): 

371 # GH#65056 

372 warnings.warn( 

373 "Inferring datetime64 from data containing " 

374 "datetime.date objects is deprecated. In a " 

375 "future version, these will be left as object " 

376 "dtype. Use pd.to_datetime to explicitly " 

377 "convert to datetime64.", 

378 Pandas4Warning, 

379 stacklevel=find_stack_level(), 

380 ) 

381 try: 

382 other = type(self)(other) 

383 except (ValueError, TypeError, OverflowError): 

384 return False 

385 

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

387 return False 

388 elif self.dtype == other.dtype: 

389 return lib.array_equivalent_bytes(self.asi8, other.asi8) 

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

391 # different units, otherwise matching 

392 try: 

393 # TODO: do this at the EA level? 

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

395 except (OutOfBoundsDatetime, OutOfBoundsTimedelta): 

396 return False 

397 else: 

398 return lib.array_equivalent_bytes(left.view("i8"), right.view("i8")) 

399 return False 

400 

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

402 """ 

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

404 

405 Parameters 

406 ---------- 

407 key : label 

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

409 

410 Returns 

411 ------- 

412 bool 

413 Whether the key search is in the index. 

414 

415 Raises 

416 ------ 

417 TypeError 

418 If the key is not hashable. 

419 

420 See Also 

421 -------- 

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

423 list-like key is in the index. 

424 

425 Examples 

426 -------- 

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

428 >>> idx 

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

430 >>> 2 in idx 

431 True 

432 >>> 6 in idx 

433 False 

434 """ 

435 hash(key) 

436 try: 

437 self.get_loc(key) 

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

439 return False 

440 return True 

441 

442 def _convert_tolerance(self, tolerance, target): 

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

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

445 

446 # -------------------------------------------------------------------- 

447 # Rendering Methods 

448 _default_na_rep = "NaT" 

449 

450 def _format_with_header( 

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

452 ) -> list[str]: 

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

454 # matches base class except for whitespace padding and date_format 

455 return header + list( 

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

457 ) 

458 

459 def _formatter_func(self, val) -> str: 

460 return self._data._formatter()(val) 

461 

462 def _format_attrs(self): 

463 """ 

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

465 """ 

466 attrs = super()._format_attrs() 

467 for attrib in self._attributes: 

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

469 if attrib == "freq": 

470 freq = self.freqstr 

471 if freq is not None: 

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

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

474 return attrs 

475 

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

477 """ 

478 Return a summarized representation. 

479 

480 Parameters 

481 ---------- 

482 name : str 

483 name to use in the summary representation 

484 

485 Returns 

486 ------- 

487 String with a summarized representation of the index 

488 """ 

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

490 if self.freq: 

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

492 

493 return result 

494 

495 # -------------------------------------------------------------------- 

496 # Indexing Methods 

497 

498 @final 

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

500 # e.g. test_getitem_setitem_periodindex 

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

502 return reso > self._resolution_obj 

503 # NB: for DTI/PI, not TDI 

504 

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

506 raise NotImplementedError 

507 

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

509 # overridden by TimedeltaIndex 

510 freqstr: str | None 

511 if self.freq is None: 

512 freqstr = None 

513 else: 

514 try: 

515 freqstr = self.freq.rule_code 

516 except NotImplementedError: 

517 # e.g. a generic DateOffset, which has no rule_code; it also 

518 # carries no quarter/month anchor, so None is the right answer 

519 freqstr = None 

520 

521 if isinstance(label, np.str_): 

522 # GH#45580 

523 label = str(label) 

524 

525 parsed, reso_str = parsing.parse_datetime_string_with_reso( 

526 label, 

527 freqstr, 

528 warn_quarter=self._warn_quarter, 

529 ) 

530 reso = Resolution.from_attrname(reso_str) 

531 return parsed, reso 

532 

533 def _get_string_slice(self, key: str) -> slice | npt.NDArray[np.intp]: # type: ignore[override] 

534 # overridden by TimedeltaIndex 

535 parsed, reso = self._parse_with_reso(key) 

536 try: 

537 return self._partial_date_slice(reso, parsed) 

538 except KeyError as err: 

539 raise KeyError(key) from err 

540 

541 @final 

542 def _partial_date_slice( 

543 self, 

544 reso: Resolution, 

545 parsed: datetime, 

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

547 """ 

548 Parameters 

549 ---------- 

550 reso : Resolution 

551 parsed : datetime 

552 

553 Returns 

554 ------- 

555 slice or ndarray[intp] 

556 """ 

557 if not self._can_partial_date_slice(reso): 

558 raise ValueError 

559 

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

561 vals = self._data._ndarray 

562 unbox = self._data._unbox 

563 

564 if self.is_monotonic_increasing: 

565 if len(self) and ( 

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

567 ): 

568 # we are out of range 

569 raise KeyError 

570 

571 # a monotonic increasing series can be sliced 

572 # (searchsorted requires ascending order) 

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

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

575 return slice(left, right) 

576 

577 elif self.is_monotonic_decreasing: 

578 if len(self) and ( 

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

580 ): 

581 # we are out of range 

582 raise KeyError 

583 

584 # searchsorted requires ascending order, so search the reversed 

585 # array and convert the indices back 

586 reversed_vals = vals[::-1] 

587 nvals = len(vals) 

588 rev_left = reversed_vals.searchsorted(unbox(t1), side="left") 

589 rev_right = reversed_vals.searchsorted(unbox(t2), side="right") 

590 return slice(nvals - rev_right, nvals - rev_left) 

591 

592 else: 

593 lhs_mask = vals >= unbox(t1) 

594 rhs_mask = vals <= unbox(t2) 

595 

596 # try to find the dates 

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

598 

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

600 """ 

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

602 

603 Parameters 

604 ---------- 

605 label : object 

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

607 

608 Returns 

609 ------- 

610 label : object 

611 

612 Notes 

613 ----- 

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

615 """ 

616 if isinstance(label, str): 

617 try: 

618 parsed, reso = self._parse_with_reso(label) 

619 except ValueError as err: 

620 # DTI -> parsing.DateParseError 

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

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

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

624 

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

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

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

628 self._raise_invalid_indexer("slice", label) 

629 

630 return label 

631 

632 # -------------------------------------------------------------------- 

633 # Arithmetic Methods 

634 

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

636 """ 

637 Shift index by desired number of time frequency increments. 

638 

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

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

641 

642 Parameters 

643 ---------- 

644 periods : int, default 1 

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

646 can be positive or negative. 

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

648 Frequency increment to shift by. 

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

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

651 

652 Returns 

653 ------- 

654 pandas.DatetimeIndex 

655 Shifted index. 

656 

657 See Also 

658 -------- 

659 Index.shift : Shift values of Index. 

660 PeriodIndex.shift : Shift values of PeriodIndex. 

661 """ 

662 raise NotImplementedError 

663 

664 # -------------------------------------------------------------------- 

665 

666 def _maybe_cast_listlike_indexer(self, keyarr): 

667 """ 

668 Analogue to maybe_cast_indexer for get_indexer instead of get_loc. 

669 """ 

670 try: 

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

672 except (ValueError, TypeError): 

673 if not isinstance(keyarr, ExtensionArray): 

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

675 res = com.asarray_tuplesafe(keyarr) 

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

677 else: 

678 res = keyarr 

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

680 

681 

682class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC): 

683 """ 

684 Mixin class for methods shared by DatetimeIndex and TimedeltaIndex, 

685 but not PeriodIndex 

686 """ 

687 

688 _data: DatetimeArray | TimedeltaArray 

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

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

691 _freq: BaseOffset | None = None 

692 

693 # Compat for frequency inference, see GH#23789 

694 _is_monotonic_increasing = Index.is_monotonic_increasing 

695 _is_monotonic_decreasing = Index.is_monotonic_decreasing 

696 _is_unique = Index.is_unique 

697 

698 @property 

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

700 """ 

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

702 

703 To learn more about the frequency strings, please see 

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

705 

706 See Also 

707 -------- 

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

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

710 

711 Examples 

712 -------- 

713 >>> datetimeindex = pd.date_range( 

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

715 ... ) 

716 >>> datetimeindex 

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

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

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

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

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

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

723 >>> datetimeindex.freq 

724 <Hour> 

725 """ 

726 return self._freq 

727 

728 @freq.setter 

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

730 arr = self._data 

731 if value is not None: 

732 value = to_offset(value) 

733 arr._validate_frequency(arr, value) 

734 if arr.dtype.kind == "m" and not isinstance(value, (Tick, Day)): 

735 raise TypeError("TimedeltaArray/Index freq must be a Tick") 

736 

737 if arr.ndim > 1: 

738 raise ValueError("Cannot set freq with ndim > 1") 

739 

740 self._freq = value 

741 

742 def astype(self, dtype, copy: bool = True): 

743 result = super().astype(dtype, copy=copy) 

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

745 # Preserve freq for unit conversions (e.g. datetime64[ns] -> [us]) 

746 result._freq = self._freq 

747 return result 

748 

749 def putmask(self, mask, value) -> Index: 

750 # GH#24555 putmask may modify values out-of-sequence; drop freq 

751 result = super().putmask(mask, value) 

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

753 result._freq = None 

754 return result 

755 

756 def _view(self) -> Self: 

757 result = super()._view() 

758 result._freq = self._freq 

759 return result 

760 

761 def copy(self, name: Hashable | None = None, deep: bool = False) -> Self: 

762 result = super().copy(name=name, deep=deep) 

763 result._freq = self._freq 

764 return result 

765 

766 def _pin_freq(self, freq, inferred) -> None: 

767 """ 

768 Constructor helper to pin the appropriate ``freq`` attribute on self. 

769 

770 ``inferred`` is the frequency inferred from the input data (or ``None`` 

771 if no freq could be inferred), as determined by the caller before 

772 wrapping the array in this Index. 

773 """ 

774 arr = self._data 

775 if freq is None: 

776 # user explicitly passed None -> override any inferred_freq 

777 self._freq = None 

778 elif freq == "infer": 

779 if inferred is not None: 

780 # already inferred during construction 

781 self._freq = inferred 

782 else: 

783 # Set _freq directly to bypass duplicative _validate_frequency 

784 # check. 

785 self._freq = to_offset(self._inferred_freq_str) 

786 elif freq is lib.no_default: 

787 # user did not specify anything, keep inferred freq if the original 

788 # data had one, otherwise leave as None (class default) 

789 self._freq = inferred 

790 elif inferred is None: 

791 # We cannot inherit a freq from the data, so we need to validate 

792 # the user-passed freq 

793 freq = to_offset(freq) 

794 type(arr)._validate_frequency(self, freq) 

795 self._freq = freq 

796 else: 

797 # Otherwise we just need to check that the user-passed freq 

798 # doesn't conflict with the one we already have. 

799 freq = to_offset(freq) 

800 if freq != inferred: 

801 # GH#61086 freq may be equivalent but not equal (e.g. 

802 # QS-FEB vs QS-MAY), so validate against the actual data. 

803 # GH#65012 checking a single step is not enough: a fixed-step 

804 # (Tick/Day) or business/custom offset can match the first gap 

805 # by coincidence yet diverge on a later step, so every step 

806 # must conform. 

807 if len(self) == 1: 

808 if not freq.is_on_offset(self[0]): 

809 raise ValueError( 

810 f"Inferred frequency {inferred} from passed " 

811 "values does not conform to passed frequency " 

812 f"{freq.freqstr}" 

813 ) 

814 elif isinstance(freq, Tick) and len(self) > 1: 

815 # A Tick is a fixed step in i8 space regardless of tz, so 

816 # conformance is just uniform spacing. This avoids the 

817 # pricier _validate_frequency (which infers the freq and 

818 # allocates a range). Not valid for Day (DST-dependent) or 

819 # calendar offsets, which stay on the path below. 

820 delta = Timedelta(freq) 

821 step = delta.as_unit(self.unit) 

822 if step != delta or not (np.diff(self.asi8) == step._value).all(): 

823 raise ValueError( 

824 f"Inferred frequency {inferred} from passed " 

825 "values does not conform to passed frequency " 

826 f"{freq.freqstr}" 

827 ) 

828 elif len(self) > 1: 

829 type(arr)._validate_frequency(self, freq) 

830 self._freq = freq 

831 

832 def _get_arithmetic_result_freq(self, other) -> BaseOffset | None: 

833 """ 

834 Check if we can preserve self.freq in addition or subtraction. 

835 """ 

836 if not lib.is_scalar(other): 

837 return None 

838 

839 # Normalize scalar types to their pandas equivalents. 

840 # Array arithmetic methods internally convert Tick/timedelta/ 

841 # np.timedelta64 → Timedelta and datetime/np.datetime64 → Timestamp. 

842 # Since we receive the original user argument, normalize here. 

843 if isinstance(other, Tick): 

844 other = Timedelta(other) 

845 elif isinstance(other, np.timedelta64): 

846 other = Timedelta(other) 

847 elif isinstance(other, np.datetime64): 

848 other = Timestamp(other) 

849 elif not isinstance(other, (Timedelta, Timestamp)): 

850 # Handles datetime.timedelta, datetime.datetime 

851 try: 

852 other = Timedelta(other) 

853 except (ValueError, TypeError, OverflowError): 

854 try: 

855 other = Timestamp(other) 

856 except (ValueError, TypeError, OverflowError): 

857 return None 

858 

859 if isinstance(self.freq, Tick): 

860 return self.freq 

861 elif self.dtype.kind == "m" and isinstance(other, Timedelta): 

862 return self.freq 

863 elif ( 

864 self.dtype.kind == "m" 

865 and isinstance(other, Timestamp) 

866 and (other.tz is None or timezones.is_utc(other.tz)) 

867 ): 

868 return self.freq 

869 elif ( 

870 lib.is_np_dtype(self.dtype, "M") 

871 and isinstance(self.freq, Day) 

872 and isinstance(other, (Timedelta, Timestamp)) 

873 ): 

874 return self.freq 

875 

876 return None 

877 

878 def _arith_method(self, other, op): 

879 result = super()._arith_method(other, op) 

880 if result is NotImplemented: 

881 return result 

882 if op in (operator.add, roperator.radd, operator.sub, roperator.rsub): 

883 new_freq = self._get_arithmetic_result_freq(other) 

884 if new_freq is not None and isinstance(result, DatetimeTimedeltaMixin): 

885 if op is roperator.rsub: 

886 new_freq = -new_freq 

887 result._freq = new_freq 

888 return result 

889 

890 def factorize( 

891 self, 

892 sort: bool = False, 

893 use_na_sentinel: bool = True, 

894 ): 

895 if self.freq is not None: 

896 # We must be unique, so can short-circuit (and retain freq) 

897 if sort and self.freq.n < 0: 

898 codes = np.arange(len(self) - 1, -1, -1, dtype=np.intp) 

899 uniques = self[::-1] 

900 else: 

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

902 uniques = self.copy() 

903 uniques._name = None 

904 return codes, uniques 

905 if len(self) > 1 and ( 

906 self.is_monotonic_increasing or self.is_monotonic_decreasing 

907 ): 

908 # Monotonic implies no NaT, so NA handling is not needed 

909 codes, uniques_indexer = algorithms.factorize_monotonic_codes( 

910 self._data._ndarray, self.is_monotonic_increasing, sort 

911 ) 

912 uniques = self[uniques_indexer] 

913 uniques._name = None 

914 return codes, uniques 

915 return super().factorize(sort=sort, use_na_sentinel=use_na_sentinel) 

916 

917 @property 

918 def unit(self) -> TimeUnit: 

919 """ 

920 The precision unit of the datetime data. 

921 

922 Returns the precision unit for the dtype of the index. This is the 

923 smallest time frame that can be stored within this dtype. 

924 

925 Returns 

926 ------- 

927 str 

928 Unit string representation (e.g. ``"ns"``). 

929 

930 See Also 

931 -------- 

932 DatetimeIndex.as_unit : Convert to the given unit. 

933 TimedeltaIndex.as_unit : Convert to the given unit. 

934 

935 Examples 

936 -------- 

937 For a DatetimeIndex: 

938 

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

940 >>> idx.unit 

941 'ns' 

942 

943 >>> idx_s = pd.DatetimeIndex(["2020-01-02 01:02:03"], dtype="datetime64[s]") 

944 >>> idx_s.unit 

945 's' 

946 

947 For a TimedeltaIndex: 

948 

949 >>> tdidx = pd.TimedeltaIndex(["1 day 3 min 2 us 42 ns"]) 

950 >>> tdidx.unit 

951 'ns' 

952 

953 >>> tdidx_s = pd.TimedeltaIndex(["1 day 3 min"], dtype="timedelta64[s]") 

954 >>> tdidx_s.unit 

955 's' 

956 """ 

957 return self._data.unit 

958 

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

960 """ 

961 Convert to a dtype with the given unit resolution. 

962 

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

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

965 resolution/precision. 

966 

967 Parameters 

968 ---------- 

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

970 

971 Returns 

972 ------- 

973 same type as self 

974 Converted to the specified unit. 

975 

976 See Also 

977 -------- 

978 Timestamp.as_unit : Convert to the given unit. 

979 Timedelta.as_unit : Convert to the given unit. 

980 DatetimeIndex.as_unit : Convert to the given unit. 

981 TimedeltaIndex.as_unit : Convert to the given unit. 

982 

983 Examples 

984 -------- 

985 For :class:`pandas.DatetimeIndex`: 

986 

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

988 >>> idx 

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

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

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

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

993 

994 For :class:`pandas.TimedeltaIndex`: 

995 

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

997 >>> tdelta_idx 

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

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

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

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

1002 """ 

1003 arr = self._data.as_unit(unit) 

1004 result = type(self)._simple_new(arr, name=self.name) 

1005 result._freq = self.freq 

1006 return result 

1007 

1008 def _with_freq(self, freq): 

1009 # GH#29843 

1010 if freq is None: 

1011 pass 

1012 elif isinstance(freq, BaseOffset): 

1013 if self.dtype.kind == "m" and not isinstance(freq, (Tick, Day)): 

1014 raise TypeError("TimedeltaArray/Index freq must be a Tick") 

1015 elif freq == "infer": 

1016 freq = to_offset(self._inferred_freq_str) 

1017 else: 

1018 raise ValueError(f"Invalid frequency: {freq!r}") 

1019 

1020 arr = self._data.view() 

1021 result = type(self)._simple_new(arr, name=self._name) 

1022 result._freq = freq 

1023 return result 

1024 

1025 @property 

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

1027 # NB: For Datetime64TZ this is lossy 

1028 if isinstance(self.dtype, DatetimeTZDtype): 

1029 warnings.warn( 

1030 "DatetimeIndex.values returning an ndarray that drops " 

1031 "timezone information is deprecated. In a future version, " 

1032 "this will return the underlying DatetimeArray instead. " 

1033 "Use 'DatetimeIndex.tz_convert(None).to_numpy()' to get a NumPy array " 

1034 "of UTC values, or 'DatetimeIndex.array' to get the ExtensionArray.\n" 

1035 "See https://pandas.pydata.org/docs/dev/whatsnew/v3.1.0.html#whatsnew-310-deprecations-values" 

1036 " for more details.", 

1037 Pandas4Warning, 

1038 # TODO bump this to stacklevel=2 in a future version 

1039 stacklevel=1, 

1040 ) 

1041 data = self._data._ndarray 

1042 data = data.view() 

1043 data.flags.writeable = False 

1044 return data 

1045 

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

1047 """ 

1048 Shift index by desired number of time frequency increments. 

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

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

1051 

1052 Parameters 

1053 ---------- 

1054 periods : int, default 1 

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

1056 can be positive or negative. 

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

1058 Frequency increment to shift by. 

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

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

1061 

1062 Returns 

1063 ------- 

1064 pandas.DatetimeIndex 

1065 Shifted index. 

1066 

1067 See Also 

1068 -------- 

1069 Index.shift : Shift values of Index. 

1070 PeriodIndex.shift : Shift values of PeriodIndex. 

1071 """ 

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

1073 if isinstance(freq, str): 

1074 freq = to_offset(freq) 

1075 offset = periods * freq 

1076 return self + offset 

1077 

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

1079 # GH#14811 empty case 

1080 return self.copy() 

1081 

1082 if self.freq is None: 

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

1084 

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

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

1087 

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

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

1090 # to be passed explicitly. 

1091 arr = self._data._generate_range( 

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

1093 ) 

1094 result = type(self)._simple_new(arr, name=self.name) 

1095 result._freq = self.freq 

1096 return result 

1097 

1098 @cache_readonly 

1099 def _inferred_freq_str(self) -> str | None: 

1100 """ 

1101 Internal version of inferred_freq without deprecation warning. 

1102 """ 

1103 return self._data._inferred_freq_str 

1104 

1105 @cache_readonly 

1106 def inferred_freq(self) -> str | BaseOffset | None: 

1107 """ 

1108 Return the inferred frequency of the index. 

1109 

1110 .. deprecated:: 3.1.0 

1111 A future version of pandas will return a :class:`BaseOffset` instead of 

1112 a string. Use 

1113 ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt 

1114 in to the future behavior. 

1115 

1116 Attempts to determine the frequency of the index by analyzing the 

1117 differences between consecutive values using ``infer_freq``. 

1118 

1119 Returns 

1120 ------- 

1121 str or None 

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

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

1124 

1125 See Also 

1126 -------- 

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

1128 otherwise ``None``. 

1129 

1130 Examples 

1131 -------- 

1132 For ``DatetimeIndex``: 

1133 

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

1135 >>> idx.inferred_freq # doctest: +SKIP 

1136 '2D' 

1137 

1138 For ``TimedeltaIndex``: 

1139 

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

1141 >>> tdelta_idx 

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

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

1144 >>> tdelta_idx.inferred_freq # doctest: +SKIP 

1145 '10D' 

1146 """ 

1147 return frequencies.maybe_convert_inferred_freq( 

1148 self._inferred_freq_str, "inferred_freq" 

1149 ) 

1150 

1151 # -------------------------------------------------------------------- 

1152 # Set Operation Methods 

1153 

1154 @cache_readonly 

1155 def _as_range_index(self) -> RangeIndex: 

1156 # Convert our i8 representations to RangeIndex 

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

1158 freq = cast("Tick", self.freq) 

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

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

1161 return RangeIndex(rng) 

1162 

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

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

1165 

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

1167 new_freq = None 

1168 if not len(res_i8): 

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

1170 new_freq = self.freq 

1171 elif isinstance(res_i8, RangeIndex): 

1172 new_freq = to_offset( 

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

1174 ) 

1175 

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

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

1178 # because test_setops_preserve_freq fails with _validate_frequency raising. 

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

1180 # be fixed by GH#41493 

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

1182 arr = type(self._data)._simple_new( 

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

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

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

1186 res_values, 

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

1188 ) 

1189 result = cast("Self", self._wrap_setop_result(other, arr)) 

1190 result._freq = new_freq 

1191 return result 

1192 

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

1194 # Dispatch to RangeIndex intersection logic. 

1195 left = self._as_range_index 

1196 right = other._as_range_index 

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

1198 return self._wrap_range_setop(other, res_i8) 

1199 

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

1201 # Dispatch to RangeIndex union logic. 

1202 left = self._as_range_index 

1203 right = other._as_range_index 

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

1205 return self._wrap_range_setop(other, res_i8) 

1206 

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

1208 """ 

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

1210 """ 

1211 other = cast("DatetimeTimedeltaMixin", other) 

1212 

1213 if self._can_range_setop(other): 

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

1215 

1216 if not self._can_fast_intersect(other): 

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

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

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

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

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

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

1223 result = self._wrap_setop_result(other, result) 

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

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

1226 

1227 else: 

1228 return self._fast_intersect(other, sort) 

1229 

1230 def _fast_intersect(self, other, sort): 

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

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

1233 left, right = self, other 

1234 else: 

1235 left, right = other, self 

1236 

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

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

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

1240 start = right[0] 

1241 

1242 if end < start: 

1243 return self[:0] 

1244 

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

1246 arr = left._values[lslice] 

1247 result = type(self)._simple_new(arr, name=self.name) 

1248 result._freq = self.freq 

1249 return result 

1250 

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

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

1253 if self.freq is None: 

1254 return False 

1255 

1256 elif other.freq != self.freq: 

1257 return False 

1258 

1259 elif not self.is_monotonic_increasing: 

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

1261 return False 

1262 

1263 freq = self.freq 

1264 

1265 # GH#44025 DateOffset (RelativeDeltaOffset) has no fixed stride, 

1266 # so matching freq alone doesn't guarantee alignment. 

1267 if isinstance(freq, RelativeDeltaOffset): 

1268 return False 

1269 

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

1271 # For |n| != 1 two same-freq ranges can be offset by a non-multiple of 

1272 # n (e.g. "2ME" starting in different months), so they need not line 

1273 # up. Rather than verify the phase, fall back to the general (slower) 

1274 # intersection. GH#42104, GH#44025 

1275 if freq.n != 1: 

1276 return False 

1277 

1278 # Below here we need actual timestamp values; cache them once 

1279 # since __getitem__ is relatively expensive on DatetimeIndex. 

1280 left_start = self[0] 

1281 right_start = other[0] 

1282 

1283 # GH#44025 For DatetimeIndex, both indices must share the same 

1284 # wall-clock time-of-day to guarantee alignment, as non-Tick 

1285 # offsets preserve wall time. (TimedeltaIndex elements are 

1286 # Timedelta, which have no time-of-day concept.) 

1287 if isinstance(left_start, Timestamp): 

1288 if (left_start.time(), left_start.nanosecond) != ( 

1289 right_start.time(), 

1290 right_start.nanosecond, 

1291 ): 

1292 return False 

1293 

1294 # GH#44025 Week(weekday=None) generates dates that depend on the 

1295 # start date: check that both indexes fall on the same day-of-week. 

1296 if isinstance(freq, Week) and freq.weekday is None: 

1297 if (left_start.toordinal() - right_start.toordinal()) % 7 != 0: 

1298 return False 

1299 

1300 return True 

1301 

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

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

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

1305 # retained on union. 

1306 freq = self.freq 

1307 

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

1309 return False 

1310 

1311 if not self.is_monotonic_increasing: 

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

1313 # TODO: do union on the reversed indexes? 

1314 return False 

1315 

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

1317 # only reached via union_many 

1318 return True 

1319 

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

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

1322 left, right = self, other 

1323 else: 

1324 left, right = other, self 

1325 

1326 right_start = right[0] 

1327 left_end = left[-1] 

1328 

1329 # Only need to "adjoin", not overlap 

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

1331 

1332 def _fast_union(self, other: Self) -> Self: 

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

1334 # and that the result is monotonic, so that it retains self.freq. 

1335 

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

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

1338 left, right = self, other 

1339 else: 

1340 left, right = other, self 

1341 

1342 left_end = left[-1] 

1343 right_end = right[-1] 

1344 

1345 # concatenate 

1346 if left_end < right_end: 

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

1348 right_chunk = right._values[loc:] 

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

1350 # The can_fast_union check ensures that the result.freq 

1351 # should match self.freq 

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

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

1354 result._freq = self.freq 

1355 return result 

1356 else: 

1357 return left 

1358 

1359 def _union(self, other, sort): 

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

1361 assert isinstance(other, type(self)) 

1362 assert self.dtype == other.dtype 

1363 

1364 if self._can_range_setop(other): 

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

1366 

1367 if self._can_fast_union(other) and (sort is not False or self[0] <= other[0]): 

1368 # the _can_fast_union check ensures that result.freq == self.freq. 

1369 # With sort=False and self starting after other, the result would 

1370 # instead be non-monotonic and carry no freq, so we leave that case 

1371 # to the generic path below. GH#66322 

1372 return self._fast_union(other) 

1373 else: 

1374 # super()._union can return an ArrayLike; wrap into an Index first 

1375 result = self._wrap_setop_result(other, super()._union(other, sort)) 

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

1377 

1378 # -------------------------------------------------------------------- 

1379 # Join Methods 

1380 

1381 def _get_join_freq(self, other): 

1382 """ 

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

1384 """ 

1385 freq = None 

1386 if self._can_fast_union(other): 

1387 freq = self.freq 

1388 return freq 

1389 

1390 def _wrap_join_result( 

1391 self, 

1392 joined, 

1393 other, 

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

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

1396 how: JoinHow, 

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

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

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

1400 joined, other, lidx, ridx, how 

1401 ) 

1402 join_index._freq = self._get_join_freq(other) 

1403 return join_index, lidx, ridx 

1404 

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

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

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

1408 

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

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

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

1412 return self._data._from_backing_data(result) 

1413 

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

1415 if ( 

1416 self.is_monotonic_increasing 

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

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

1419 ): 

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

1421 # This is needed for GH#63262 

1422 if side == "right": 

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

1424 else: 

1425 # round up 

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

1427 

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

1429 

1430 # -------------------------------------------------------------------- 

1431 # List-like Methods 

1432 

1433 def _getitem_slice(self, slobj: slice) -> Self: 

1434 result = super()._getitem_slice(slobj) 

1435 result._freq = self._get_getitem_freq(slobj) 

1436 return result 

1437 

1438 def __getitem__(self, key): 

1439 result = super().__getitem__(key) 

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

1441 result._freq = self._get_getitem_freq(key) 

1442 return result 

1443 

1444 def _get_getitem_freq(self, key) -> BaseOffset | None: 

1445 """ 

1446 Find the `freq` attribute to assign to the result of a __getitem__ lookup. 

1447 """ 

1448 key = check_array_indexer(self._data, key) # maybe ndarray[bool] -> slice 

1449 freq = None 

1450 if isinstance(key, slice): 

1451 if self.freq is not None and key.step is not None: 

1452 freq = key.step * self.freq 

1453 else: 

1454 freq = self.freq 

1455 elif key is Ellipsis: 

1456 # GH#21282 indexing with Ellipsis is similar to a full slice, 

1457 # should preserve `freq` attribute 

1458 freq = self.freq 

1459 elif com.is_bool_indexer(key): 

1460 new_key = lib.maybe_booleans_to_slice(key.view(np.uint8)) 

1461 if isinstance(new_key, slice): 

1462 return self._get_getitem_freq(new_key) 

1463 return freq 

1464 

1465 def _concat(self, to_concat: list[Index], name: Hashable) -> Index: 

1466 result = super()._concat(to_concat, name) 

1467 

1468 # GH#3232: If the concat result is evenly spaced, we can retain the 

1469 # original frequency 

1470 obj = cast("DatetimeTimedeltaMixin", to_concat[0]) 

1471 to_concat_nonempty = cast( 

1472 "list[DatetimeTimedeltaMixin]", 

1473 [idx for idx in to_concat if len(idx)], 

1474 ) 

1475 if ( 

1476 isinstance(result, type(self)) 

1477 and obj.freq is not None 

1478 and all(idx.freq == obj.freq for idx in to_concat_nonempty) 

1479 ): 

1480 freq = obj.freq 

1481 tz = getattr(self.dtype, "tz", None) 

1482 if ( 

1483 isinstance(freq, Tick) or (tz is None and isinstance(freq, Day)) 

1484 ) and all(idx.unit == self.unit for idx in to_concat_nonempty): 

1485 # freq is a fixed delta in the stored i8 representation, so we 

1486 # can check boundary continuity without boxing endpoints to 

1487 # Timestamps and doing per-pair offset arithmetic. 

1488 step = Timedelta(freq).as_unit(self.unit)._value 

1489 i8s = [idx._data._ndarray.view("i8") for idx in to_concat_nonempty] 

1490 evenly_spaced = all(a[-1] + step == b[0] for a, b in pairwise(i8s)) 

1491 else: 

1492 evenly_spaced = all( 

1493 pair[0][-1] + freq == pair[1][0] 

1494 for pair in pairwise(to_concat_nonempty) 

1495 ) 

1496 if evenly_spaced: 

1497 result._freq = freq 

1498 

1499 return result 

1500 

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

1502 """ 

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

1504 """ 

1505 freq = None 

1506 if self.freq is not None: 

1507 if is_integer(loc): 

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

1509 freq = self.freq 

1510 else: 

1511 if is_list_like(loc): 

1512 # error: Incompatible types in assignment (expression has 

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

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

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

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

1517 ) 

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

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

1520 freq = self.freq 

1521 return freq 

1522 

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

1524 """ 

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

1526 """ 

1527 value = self._data._validate_scalar(item) 

1528 item = self._data._box_func(value) 

1529 

1530 freq = None 

1531 if self.freq is not None: 

1532 # freq can be preserved on edge cases 

1533 if self.size: 

1534 if item is NaT: 

1535 pass 

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

1537 freq = self.freq 

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

1539 freq = self.freq 

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

1541 elif isinstance(self.freq, Tick): 

1542 # all TimedeltaIndex cases go through here; is_on_offset 

1543 # would raise TypeError 

1544 freq = self.freq 

1545 elif self.freq.is_on_offset(item): 

1546 freq = self.freq 

1547 return freq 

1548 

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

1550 """ 

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

1552 

1553 Parameters 

1554 ---------- 

1555 loc : int or list of int 

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

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

1558 

1559 Returns 

1560 ------- 

1561 Index 

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

1563 

1564 See Also 

1565 -------- 

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

1567 

1568 Examples 

1569 -------- 

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

1571 >>> idx.delete(1) 

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

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

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

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

1576 """ 

1577 result = super().delete(loc) 

1578 result._freq = self._get_delete_freq(loc) 

1579 return result 

1580 

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

1582 """ 

1583 Make new Index inserting new item at location. 

1584 Follows Python numpy.insert semantics for negative values. 

1585 

1586 Parameters 

1587 ---------- 

1588 loc : int 

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

1590 item : object 

1591 The new item to be inserted into the Index. 

1592 

1593 Returns 

1594 ------- 

1595 Index 

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

1597 the specified location within the original Index. 

1598 

1599 See Also 

1600 -------- 

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

1602 

1603 Examples 

1604 -------- 

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

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

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

1608 """ 

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

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

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

1612 result._freq = self._get_insert_freq(loc, item) 

1613 return result 

1614 

1615 # -------------------------------------------------------------------- 

1616 # NDArray-Like Methods 

1617 

1618 def take( 

1619 self, 

1620 indices, 

1621 axis: Axis = 0, 

1622 allow_fill: bool | lib.NoDefault = lib.no_default, 

1623 fill_value=lib.no_default, 

1624 **kwargs, 

1625 ) -> Self: 

1626 """ 

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

1628 

1629 For internal compatibility with numpy arrays. 

1630 

1631 Parameters 

1632 ---------- 

1633 indices : array-like 

1634 Indices to be taken. 

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

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

1637 allow_fill : bool, optional 

1638 How to handle negative values in `indices`. 

1639 

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

1641 from the right, matching :func:`numpy.take`. 

1642 * True: negative values in `indices` indicate missing values. ``-1`` 

1643 entries are set to ``fill_value`` (defaulting to ``NaT`` if not 

1644 supplied). Any other negative values raise a ``ValueError``. 

1645 * Not supplied: defaults to ``allow_fill=False`` unless 

1646 ``fill_value`` is explicitly provided, in which case fill 

1647 semantics apply (``allow_fill=True``). 

1648 fill_value : scalar, optional 

1649 If fill semantics apply (see ``allow_fill``), indices specified by 

1650 ``-1`` are filled with ``fill_value``. Passing ``fill_value=None`` 

1651 is equivalent to passing ``self._na_value`` (``NaT``). 

1652 **kwargs 

1653 Required for compatibility with numpy. 

1654 

1655 Returns 

1656 ------- 

1657 Index 

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

1659 type as self, except for RangeIndex. 

1660 

1661 See Also 

1662 -------- 

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

1664 elements of a at the given indices. 

1665 

1666 Examples 

1667 -------- 

1668 >>> idx = pd.date_range("2024-01-01", periods=3) 

1669 >>> idx.take([2, 1, 0]) 

1670 DatetimeIndex(['2024-01-03', '2024-01-02', '2024-01-01'], 

1671 dtype='datetime64[us]', freq='-1D') 

1672 """ 

1673 nv.validate_take((), kwargs) 

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

1675 

1676 result = NDArrayBackedExtensionIndex.take( 

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

1678 ) 

1679 

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

1681 if isinstance(maybe_slice, slice): 

1682 freq = self._get_getitem_freq(maybe_slice) 

1683 result._freq = freq 

1684 return result