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

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

991 statements  

1from __future__ import annotations 

2 

3from datetime import ( 

4 datetime, 

5 timedelta, 

6) 

7from functools import wraps 

8import operator 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 Literal, 

13 Self, 

14 TypeAlias, 

15 Union, 

16 cast, 

17 final, 

18 overload, 

19) 

20import warnings 

21 

22import numpy as np 

23 

24from pandas._config import using_string_dtype 

25from pandas._config.config import get_option 

26 

27from pandas._libs import ( 

28 algos, 

29 lib, 

30) 

31from pandas._libs.tslibs import ( 

32 BaseOffset, 

33 Day, 

34 IncompatibleFrequency, 

35 NaT, 

36 NaTType, 

37 Period, 

38 Resolution, 

39 Tick, 

40 Timedelta, 

41 Timestamp, 

42 add_overflowsafe, 

43 astype_overflowsafe, 

44 get_unit_from_dtype, 

45 iNaT, 

46 ints_to_pydatetime, 

47 ints_to_pytimedelta, 

48 periods_per_day, 

49 timezones, 

50 to_offset, 

51) 

52from pandas._libs.tslibs.fields import ( 

53 RoundTo, 

54 round_nsint64, 

55) 

56from pandas._libs.tslibs.np_datetime import compare_mismatched_resolutions 

57from pandas._libs.tslibs.timedeltas import get_unit_for_round 

58from pandas._libs.tslibs.timestamps import integer_op_not_supported 

59from pandas._typing import ( 

60 ArrayLike, 

61 AxisInt, 

62 DatetimeLikeScalar, 

63 Dtype, 

64 DtypeObj, 

65 F, 

66 InterpolateOptions, 

67 NpDtype, 

68 PositionalIndexer2D, 

69 PositionalIndexerTuple, 

70 ScalarIndexer, 

71 SequenceIndexer, 

72 TakeIndexer, 

73 TimeAmbiguous, 

74 TimeNonexistent, 

75 npt, 

76) 

77from pandas.compat.numpy import function as nv 

78from pandas.errors import ( 

79 AbstractMethodError, 

80 InvalidComparison, 

81 PerformanceWarning, 

82) 

83from pandas.util._decorators import ( 

84 cache_readonly, 

85) 

86from pandas.util._exceptions import find_stack_level 

87 

88from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike 

89from pandas.core.dtypes.common import ( 

90 is_all_strings, 

91 is_integer_dtype, 

92 is_list_like, 

93 is_object_dtype, 

94 is_string_dtype, 

95 pandas_dtype, 

96) 

97from pandas.core.dtypes.dtypes import ( 

98 ArrowDtype, 

99 CategoricalDtype, 

100 DatetimeTZDtype, 

101 ExtensionDtype, 

102 PeriodDtype, 

103) 

104from pandas.core.dtypes.generic import ( 

105 ABCCategorical, 

106 ABCMultiIndex, 

107) 

108from pandas.core.dtypes.missing import ( 

109 is_valid_na_for_dtype, 

110 isna, 

111) 

112 

113from pandas.core import ( 

114 algorithms, 

115 missing, 

116 nanops, 

117 ops, 

118) 

119from pandas.core.algorithms import ( 

120 isin, 

121 map_array, 

122 unique1d, 

123) 

124from pandas.core.array_algos import datetimelike_accumulations 

125from pandas.core.arraylike import OpsMixin 

126from pandas.core.arrays._mixins import ( 

127 NDArrayBackedExtensionArray, 

128 ravel_compat, 

129) 

130from pandas.core.arrays.arrow.array import ArrowExtensionArray 

131from pandas.core.arrays.base import ExtensionArray 

132from pandas.core.arrays.integer import IntegerArray 

133import pandas.core.common as com 

134from pandas.core.construction import ( 

135 array as pd_array, 

136 ensure_wrapped_if_datetimelike, 

137 extract_array, 

138) 

139from pandas.core.indexers import ( 

140 check_array_indexer, 

141 check_setitem_lengths, 

142) 

143from pandas.core.ops.common import unpack_zerodim_and_defer 

144from pandas.core.ops.invalid import ( 

145 invalid_comparison, 

146 make_invalid_op, 

147) 

148 

149from pandas.tseries import frequencies 

150 

151if TYPE_CHECKING: 

152 from collections.abc import ( 

153 Callable, 

154 Iterator, 

155 Sequence, 

156 ) 

157 

158 from pandas._typing import TimeUnit 

159 

160 from pandas import Index 

161 from pandas.core.arrays import ( 

162 DatetimeArray, 

163 PeriodArray, 

164 TimedeltaArray, 

165 ) 

166 

167DTScalarOrNaT: TypeAlias = DatetimeLikeScalar | NaTType 

168 

169 

170def _make_unpacked_invalid_op(op_name: str): 

171 op = make_invalid_op(op_name) 

172 return unpack_zerodim_and_defer(op_name)(op) 

173 

174 

175def _period_dispatch(meth: F) -> F: 

176 """ 

177 For PeriodArray methods, dispatch to DatetimeArray and re-wrap the results 

178 in PeriodArray. We cannot use ._ndarray directly for the affected 

179 methods because the i8 data has different semantics on NaT values. 

180 """ 

181 

182 @wraps(meth) 

183 def new_meth(self, *args, **kwargs): 

184 if not isinstance(self.dtype, PeriodDtype): 

185 return meth(self, *args, **kwargs) 

186 

187 arr = self.view("M8[ns]") 

188 result = meth(arr, *args, **kwargs) 

189 if result is NaT: 

190 return NaT 

191 elif isinstance(result, Timestamp): 

192 return self._box_func(result._value) 

193 

194 res_i8 = result.view("i8") 

195 return self._from_backing_data(res_i8) 

196 

197 return cast(F, new_meth) 

198 

199 

200class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray): 

201 """ 

202 Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray 

203 

204 Assumes that __new__/__init__ defines: 

205 _ndarray 

206 

207 and that inheriting subclass implements: 

208 freq 

209 """ 

210 

211 # _infer_matches -> which infer_dtype strings are close enough to our own 

212 _infer_matches: tuple[str, ...] 

213 _is_recognized_dtype: Callable[[DtypeObj], bool] 

214 _recognized_scalars: tuple[type, ...] 

215 _ndarray: np.ndarray 

216 freq: BaseOffset | None 

217 

218 @cache_readonly 

219 def _can_hold_na(self) -> bool: 

220 return True 

221 

222 def __init__( 

223 self, data, dtype: Dtype | None = None, freq=None, copy: bool = False 

224 ) -> None: 

225 raise AbstractMethodError(self) 

226 

227 @property 

228 def _scalar_type(self) -> type[DatetimeLikeScalar]: 

229 """ 

230 The scalar associated with this datelike 

231 

232 * PeriodArray : Period 

233 * DatetimeArray : Timestamp 

234 * TimedeltaArray : Timedelta 

235 """ 

236 raise AbstractMethodError(self) 

237 

238 def _scalar_from_string(self, value: str) -> DTScalarOrNaT: 

239 """ 

240 Construct a scalar type from a string. 

241 

242 Parameters 

243 ---------- 

244 value : str 

245 

246 Returns 

247 ------- 

248 Period, Timestamp, or Timedelta, or NaT 

249 Whatever the type of ``self._scalar_type`` is. 

250 

251 Notes 

252 ----- 

253 This should call ``self._check_compatible_with`` before 

254 unboxing the result. 

255 """ 

256 raise AbstractMethodError(self) 

257 

258 def _unbox_scalar( 

259 self, value: DTScalarOrNaT 

260 ) -> np.int64 | np.datetime64 | np.timedelta64: 

261 """ 

262 Unbox the integer value of a scalar `value`. 

263 

264 Parameters 

265 ---------- 

266 value : Period, Timestamp, Timedelta, or NaT 

267 Depending on subclass. 

268 

269 Returns 

270 ------- 

271 int 

272 

273 Examples 

274 -------- 

275 >>> arr = pd.array(np.array(["1970-01-01"], "datetime64[ns]")) 

276 >>> arr._unbox_scalar(arr[0]) 

277 np.datetime64('1970-01-01T00:00:00.000000000') 

278 """ 

279 raise AbstractMethodError(self) 

280 

281 def _check_compatible_with(self, other: DTScalarOrNaT) -> None: 

282 """ 

283 Verify that `self` and `other` are compatible. 

284 

285 * DatetimeArray verifies that the timezones (if any) match 

286 * PeriodArray verifies that the freq matches 

287 * Timedelta has no verification 

288 

289 In each case, NaT is considered compatible. 

290 

291 Parameters 

292 ---------- 

293 other 

294 

295 Raises 

296 ------ 

297 Exception 

298 """ 

299 raise AbstractMethodError(self) 

300 

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

302 

303 def _box_func(self, x): 

304 """ 

305 box function to get object from internal representation 

306 """ 

307 raise AbstractMethodError(self) 

308 

309 def _box_values(self, values) -> np.ndarray: 

310 """ 

311 apply box func to passed values 

312 """ 

313 return lib.map_infer(values, self._box_func, convert=False) 

314 

315 def __iter__(self) -> Iterator: 

316 if self.ndim > 1: 

317 return (self[n] for n in range(len(self))) 

318 else: 

319 return (self._box_func(v) for v in self.asi8) 

320 

321 @property 

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

323 """ 

324 Integer representation of the values. 

325 

326 Returns 

327 ------- 

328 ndarray 

329 An ndarray with int64 dtype. 

330 """ 

331 # do not cache or you'll create a memory leak 

332 return self._ndarray.view("i8") 

333 

334 # ---------------------------------------------------------------- 

335 # Rendering Methods 

336 

337 def _format_native_types( 

338 self, *, na_rep: str | float = "NaT", date_format=None 

339 ) -> npt.NDArray[np.object_]: 

340 """ 

341 Helper method for astype when converting to strings. 

342 

343 Returns 

344 ------- 

345 ndarray[str] 

346 """ 

347 raise AbstractMethodError(self) 

348 

349 def _formatter(self, boxed: bool = False) -> Callable[[object], str]: 

350 # TODO: Remove Datetime & DatetimeTZ formatters. 

351 return "'{}'".format 

352 

353 # ---------------------------------------------------------------- 

354 # Array-Like / EA-Interface Methods 

355 

356 def __array__( 

357 self, dtype: NpDtype | None = None, copy: bool | None = None 

358 ) -> np.ndarray: 

359 # used for Timedelta/DatetimeArray, overwritten by PeriodArray 

360 if is_object_dtype(dtype): 

361 if copy is False: 

362 raise ValueError( 

363 "Unable to avoid copy while creating an array as requested." 

364 ) 

365 return np.array(list(self), dtype=object) 

366 

367 if copy is True: 

368 return np.array(self._ndarray, dtype=dtype) 

369 

370 result = self._ndarray 

371 if self._readonly: 

372 result = result.view() 

373 result.flags.writeable = False 

374 return result 

375 

376 @overload 

377 def __getitem__(self, key: ScalarIndexer) -> DTScalarOrNaT: ... 

378 

379 @overload 

380 def __getitem__( 

381 self, 

382 key: SequenceIndexer | PositionalIndexerTuple, 

383 ) -> Self: ... 

384 

385 def __getitem__(self, key: PositionalIndexer2D) -> Self | DTScalarOrNaT: 

386 """ 

387 This getitem defers to the underlying array, which by-definition can 

388 only handle list-likes, slices, and integer scalars 

389 """ 

390 # Use cast as we know we will get back a DatetimeLikeArray or DTScalar, 

391 # but skip evaluating the Union at runtime for performance 

392 # (see https://github.com/pandas-dev/pandas/pull/44624) 

393 result = cast(Union[Self, DTScalarOrNaT], super().__getitem__(key)) 

394 if lib.is_scalar(result): 

395 return result 

396 else: 

397 # At this point we know the result is an array. 

398 result = cast(Self, result) 

399 # error: Incompatible types in assignment (expression has type 

400 # "BaseOffset | None", variable has type "None") 

401 result._freq = self._get_getitem_freq(key) # type: ignore[assignment] 

402 return result 

403 

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

405 """ 

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

407 """ 

408 is_period = isinstance(self.dtype, PeriodDtype) 

409 if is_period: 

410 freq = self.freq 

411 elif self.ndim != 1: 

412 freq = None 

413 else: 

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

415 freq = None 

416 if isinstance(key, slice): 

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

418 freq = key.step * self.freq 

419 else: 

420 freq = self.freq 

421 elif key is Ellipsis: 

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

423 # should preserve `freq` attribute 

424 freq = self.freq 

425 elif com.is_bool_indexer(key): 

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

427 if isinstance(new_key, slice): 

428 return self._get_getitem_freq(new_key) 

429 return freq 

430 

431 # error: Argument 1 of "__setitem__" is incompatible with supertype 

432 # "ExtensionArray"; supertype defines the argument type as "Union[int, 

433 # ndarray]" 

434 def __setitem__( 

435 self, 

436 key: int | Sequence[int] | Sequence[bool] | slice, 

437 value: NaTType | Any | Sequence[Any], 

438 ) -> None: 

439 # I'm fudging the types a bit here. "Any" above really depends 

440 # on type(self). For PeriodArray, it's Period (or stuff coercible 

441 # to a period in from_sequence). For DatetimeArray, it's Timestamp... 

442 # I don't know if mypy can do that, possibly with Generics. 

443 # https://mypy.readthedocs.io/en/latest/generics.html 

444 

445 no_op = check_setitem_lengths(key, value, self) 

446 

447 # Calling super() before the no_op short-circuit means that we raise 

448 # on invalid 'value' even if this is a no-op, e.g. wrong-dtype empty array. 

449 super().__setitem__(key, value) 

450 

451 if no_op: 

452 return 

453 

454 self._maybe_clear_freq() 

455 

456 def _maybe_clear_freq(self) -> None: 

457 # inplace operations like __setitem__ may invalidate the freq of 

458 # DatetimeArray and TimedeltaArray 

459 pass 

460 

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

462 # Some notes on cases we don't have to handle here in the base class: 

463 # 1. PeriodArray.astype handles period -> period 

464 # 2. DatetimeArray.astype handles conversion between tz. 

465 # 3. DatetimeArray.astype handles datetime -> period 

466 dtype = pandas_dtype(dtype) 

467 

468 if dtype == object: 

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

470 self = cast("DatetimeArray", self) 

471 # *much* faster than self._box_values 

472 # for e.g. test_get_loc_tuple_monotonic_above_size_cutoff 

473 i8data = self.asi8 

474 converted = ints_to_pydatetime( 

475 i8data, 

476 tz=self.tz, 

477 box="timestamp", 

478 reso=self._creso, 

479 ) 

480 return converted 

481 

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

483 return ints_to_pytimedelta(self._ndarray, box=True) 

484 

485 return self._box_values(self.asi8.ravel()).reshape(self.shape) 

486 

487 elif is_string_dtype(dtype): 

488 if isinstance(dtype, ExtensionDtype): 

489 arr_object = self._format_native_types(na_rep=dtype.na_value) # type: ignore[arg-type] 

490 cls = dtype.construct_array_type() 

491 return cls._from_sequence(arr_object, dtype=dtype, copy=False) 

492 else: 

493 return self._format_native_types() 

494 

495 elif isinstance(dtype, ExtensionDtype): 

496 return super().astype(dtype, copy=copy) 

497 elif dtype.kind in "iu": 

498 # we deliberately ignore int32 vs. int64 here. 

499 # See https://github.com/pandas-dev/pandas/issues/24381 for more. 

500 values = self.asi8 

501 if dtype != np.int64: 

502 raise TypeError( 

503 f"Converting from {self.dtype} to {dtype} is not supported. " 

504 "Do obj.astype('int64').astype(dtype) instead" 

505 ) 

506 

507 if copy: 

508 values = values.copy() 

509 return values 

510 elif (dtype.kind in "mM" and self.dtype != dtype) or dtype.kind == "f": 

511 # disallow conversion between datetime/timedelta, 

512 # and conversions for any datetimelike to float 

513 msg = f"Cannot cast {type(self).__name__} to dtype {dtype}" 

514 raise TypeError(msg) 

515 else: 

516 return np.asarray(self, dtype=dtype) 

517 

518 @overload # type: ignore[override] 

519 def view(self) -> Self: ... 

520 

521 @overload 

522 def view(self, dtype: Literal["M8[ns]"]) -> DatetimeArray: ... 

523 

524 @overload 

525 def view(self, dtype: Literal["m8[ns]"]) -> TimedeltaArray: ... 

526 

527 @overload 

528 def view(self, dtype: Dtype | None = ...) -> ArrayLike: ... 

529 

530 def view(self, dtype: Dtype | None = None) -> ArrayLike: 

531 # we need to explicitly call super() method as long as the `@overload`s 

532 # are present in this file. 

533 return super().view(dtype) 

534 

535 def _putmask(self, mask: npt.NDArray[np.bool_], value) -> None: 

536 super()._putmask(mask, value) 

537 self._freq = None # GH#24555 

538 

539 # ------------------------------------------------------------------ 

540 # Validation Methods 

541 # TODO: try to de-duplicate these, ensure identical behavior 

542 

543 def _validate_comparison_value(self, other): 

544 if isinstance(other, str): 

545 try: 

546 # GH#18435 strings get a pass from tzawareness compat 

547 other = self._scalar_from_string(other) 

548 except (ValueError, IncompatibleFrequency) as err: 

549 # failed to parse as Timestamp/Timedelta/Period 

550 raise InvalidComparison(other) from err 

551 

552 if isinstance(other, self._recognized_scalars) or other is NaT: 

553 other = self._scalar_type(other) 

554 try: 

555 self._check_compatible_with(other) 

556 except TypeError as err: 

557 # e.g. tzawareness mismatch 

558 raise InvalidComparison(other) from err 

559 

560 elif not is_list_like(other): 

561 raise InvalidComparison(other) 

562 

563 elif len(other) != len(self): 

564 raise ValueError("Lengths must match") 

565 

566 else: 

567 try: 

568 other = self._validate_listlike(other, allow_object=True) 

569 self._check_compatible_with(other) 

570 except TypeError as err: 

571 if is_object_dtype(getattr(other, "dtype", None)): 

572 # We will have to operate element-wise 

573 pass 

574 else: 

575 raise InvalidComparison(other) from err 

576 

577 return other 

578 

579 def _validate_scalar( 

580 self, 

581 value, 

582 *, 

583 allow_listlike: bool = False, 

584 unbox: bool = True, 

585 ): 

586 """ 

587 Validate that the input value can be cast to our scalar_type. 

588 

589 Parameters 

590 ---------- 

591 value : object 

592 allow_listlike: bool, default False 

593 When raising an exception, whether the message should say 

594 listlike inputs are allowed. 

595 unbox : bool, default True 

596 Whether to unbox the result before returning. Note: unbox=False 

597 skips the setitem compatibility check. 

598 

599 Returns 

600 ------- 

601 self._scalar_type or NaT 

602 """ 

603 if isinstance(value, self._scalar_type): 

604 pass 

605 

606 elif isinstance(value, str): 

607 # NB: Careful about tzawareness 

608 try: 

609 value = self._scalar_from_string(value) 

610 except ValueError as err: 

611 msg = self._validation_error_message(value, allow_listlike) 

612 raise TypeError(msg) from err 

613 

614 elif is_valid_na_for_dtype(value, self.dtype): 

615 # GH#18295 

616 value = NaT 

617 

618 elif isna(value): 

619 # if we are dt64tz and value is dt64("NaT"), dont cast to NaT, 

620 # or else we'll fail to raise in _unbox_scalar 

621 msg = self._validation_error_message(value, allow_listlike) 

622 raise TypeError(msg) 

623 

624 elif isinstance(value, self._recognized_scalars): 

625 # error: Argument 1 to "Timestamp" has incompatible type "object"; expected 

626 # "integer[Any] | float | str | date | datetime | datetime64" 

627 value = self._scalar_type(value) # type: ignore[arg-type] 

628 

629 else: 

630 msg = self._validation_error_message(value, allow_listlike) 

631 raise TypeError(msg) 

632 

633 if not unbox: 

634 # NB: In general NDArrayBackedExtensionArray will unbox here; 

635 # this option exists to prevent a performance hit in 

636 # TimedeltaIndex.get_loc 

637 return value 

638 return self._unbox_scalar(value) 

639 

640 def _validation_error_message(self, value, allow_listlike: bool = False) -> str: 

641 """ 

642 Construct an exception message on validation error. 

643 

644 Some methods allow only scalar inputs, while others allow either scalar 

645 or listlike. 

646 

647 Parameters 

648 ---------- 

649 allow_listlike: bool, default False 

650 

651 Returns 

652 ------- 

653 str 

654 """ 

655 if hasattr(value, "dtype") and getattr(value, "ndim", 0) > 0: 

656 msg_got = f"{value.dtype} array" 

657 else: 

658 msg_got = f"'{type(value).__name__}'" 

659 if allow_listlike: 

660 msg = ( 

661 f"value should be a '{self._scalar_type.__name__}', 'NaT', " 

662 f"or array of those. Got {msg_got} instead." 

663 ) 

664 else: 

665 msg = ( 

666 f"value should be a '{self._scalar_type.__name__}' or 'NaT'. " 

667 f"Got {msg_got} instead." 

668 ) 

669 return msg 

670 

671 def _validate_listlike(self, value, allow_object: bool = False): 

672 if isinstance(value, type(self)): 

673 if self.dtype.kind in "mM" and not allow_object and self.unit != value.unit: # type: ignore[attr-defined] 

674 # error: "DatetimeLikeArrayMixin" has no attribute "as_unit" 

675 value = value.as_unit(self.unit, round_ok=False) # type: ignore[attr-defined] 

676 return value 

677 

678 if isinstance(value, list) and len(value) == 0: 

679 # We treat empty list as our own dtype. 

680 return type(self)._from_sequence([], dtype=self.dtype) 

681 

682 if hasattr(value, "dtype") and value.dtype == object: 

683 # `array` below won't do inference if value is an Index or Series. 

684 # so do so here. in the Index case, inferred_type may be cached. 

685 if lib.infer_dtype(value) in self._infer_matches: 

686 try: 

687 value = type(self)._from_sequence(value) 

688 except (ValueError, TypeError) as err: 

689 if allow_object: 

690 return value 

691 msg = self._validation_error_message(value, True) 

692 raise TypeError(msg) from err 

693 

694 if isinstance(value, list): 

695 value = construct_1d_object_array_from_listlike(value) 

696 if isinstance(value, np.ndarray) and value.dtype == object: 

697 # We need to call maybe_convert_objects here instead of in pd_array 

698 # so we can specify dtype_if_all_nat. 

699 value = lib.maybe_convert_objects( 

700 value, convert_non_numeric=True, dtype_if_all_nat=self.dtype 

701 ) 

702 # Do type inference if necessary up front (after unpacking 

703 # NumpyExtensionArray) 

704 # e.g. we passed PeriodIndex.values and got an ndarray of Periods 

705 value = extract_array(value, extract_numpy=True) 

706 value = pd_array(value) 

707 value = extract_array(value, extract_numpy=True) 

708 

709 if is_all_strings(value): 

710 # We got a StringArray 

711 try: 

712 # TODO: Could use from_sequence_of_strings if implemented 

713 # Note: passing dtype is necessary for PeriodArray tests 

714 value = type(self)._from_sequence(value, dtype=self.dtype) 

715 except ValueError: 

716 pass 

717 

718 if isinstance(value.dtype, CategoricalDtype): 

719 # e.g. we have a Categorical holding self.dtype 

720 if value.categories.dtype == self.dtype: 

721 # TODO: do we need equal dtype or just comparable? 

722 value = value._internal_get_values() 

723 value = extract_array(value, extract_numpy=True) 

724 

725 if allow_object and is_object_dtype(value.dtype): 

726 pass 

727 

728 elif not type(self)._is_recognized_dtype(value.dtype): 

729 msg = self._validation_error_message(value, True) 

730 raise TypeError(msg) 

731 

732 if self.dtype.kind in "mM" and not allow_object: 

733 # error: "DatetimeLikeArrayMixin" has no attribute "as_unit" 

734 value = value.as_unit(self.unit, round_ok=False) # type: ignore[attr-defined] 

735 return value 

736 

737 def _validate_setitem_value(self, value): 

738 if is_list_like(value): 

739 value = self._validate_listlike(value) 

740 else: 

741 return self._validate_scalar(value, allow_listlike=True) 

742 

743 return self._unbox(value) 

744 

745 @final 

746 def _unbox(self, other) -> np.int64 | np.datetime64 | np.timedelta64 | np.ndarray: 

747 """ 

748 Unbox either a scalar with _unbox_scalar or an instance of our own type. 

749 """ 

750 if lib.is_scalar(other): 

751 other = self._unbox_scalar(other) 

752 else: 

753 # same type as self 

754 self._check_compatible_with(other) 

755 other = other._ndarray 

756 return other 

757 

758 # ------------------------------------------------------------------ 

759 # Additional array methods 

760 # These are not part of the EA API, but we implement them because 

761 # pandas assumes they're there. 

762 

763 @ravel_compat 

764 def map(self, mapper, na_action: Literal["ignore"] | None = None): 

765 from pandas import Index 

766 

767 result = map_array(self, mapper, na_action=na_action) 

768 result = Index(result) 

769 

770 if isinstance(result, ABCMultiIndex): 

771 return result.to_numpy() 

772 else: 

773 return result.array 

774 

775 def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]: 

776 """ 

777 Compute boolean array of whether each value is found in the 

778 passed set of values. 

779 

780 Parameters 

781 ---------- 

782 values : np.ndarray or ExtensionArray 

783 

784 Returns 

785 ------- 

786 ndarray[bool] 

787 """ 

788 if values.dtype.kind in "fiuc": 

789 # TODO: de-duplicate with equals, validate_comparison_value 

790 return np.zeros(self.shape, dtype=bool) 

791 

792 values = ensure_wrapped_if_datetimelike(values) 

793 

794 if not isinstance(values, type(self)): 

795 if values.dtype == object: 

796 values = lib.maybe_convert_objects( 

797 values, # type: ignore[arg-type] 

798 convert_non_numeric=True, 

799 dtype_if_all_nat=self.dtype, 

800 ) 

801 if values.dtype != object: 

802 return self.isin(values) 

803 else: 

804 # TODO: Deprecate this case 

805 # https://github.com/pandas-dev/pandas/pull/58645/files#r1604055791 

806 return isin(self.astype(object), values) 

807 return np.zeros(self.shape, dtype=bool) 

808 

809 if self.dtype.kind in "mM": 

810 self = cast("DatetimeArray | TimedeltaArray", self) 

811 # error: "DatetimeLikeArrayMixin" has no attribute "as_unit" 

812 values = values.as_unit(self.unit) # type: ignore[attr-defined] 

813 

814 try: 

815 # error: Argument 1 to "_check_compatible_with" of "DatetimeLikeArrayMixin" 

816 # has incompatible type "ExtensionArray | ndarray[Any, Any]"; expected 

817 # "Period | Timestamp | Timedelta | NaTType" 

818 self._check_compatible_with(values) # type: ignore[arg-type] 

819 except (TypeError, ValueError): 

820 # Includes tzawareness mismatch and IncompatibleFrequencyError 

821 return np.zeros(self.shape, dtype=bool) 

822 

823 # error: Item "ExtensionArray" of "ExtensionArray | ndarray[Any, Any]" 

824 # has no attribute "asi8" 

825 return isin(self.asi8, values.asi8) # type: ignore[union-attr] 

826 

827 # ------------------------------------------------------------------ 

828 # Null Handling 

829 

830 def isna(self) -> npt.NDArray[np.bool_]: 

831 return self._isnan 

832 

833 @property # NB: override with cache_readonly in immutable subclasses 

834 def _isnan(self) -> npt.NDArray[np.bool_]: 

835 """ 

836 return if each value is nan 

837 """ 

838 return self.asi8 == iNaT 

839 

840 @property # NB: override with cache_readonly in immutable subclasses 

841 def _hasna(self) -> bool: 

842 """ 

843 return if I have any nans; enables various perf speedups 

844 """ 

845 return bool(self._isnan.any()) 

846 

847 def _maybe_mask_results( 

848 self, result: np.ndarray, fill_value=iNaT, convert=None 

849 ) -> np.ndarray: 

850 """ 

851 Parameters 

852 ---------- 

853 result : np.ndarray 

854 fill_value : object, default iNaT 

855 convert : str, dtype or None 

856 

857 Returns 

858 ------- 

859 result : ndarray with values replace by the fill_value 

860 

861 mask the result if needed, convert to the provided dtype if its not 

862 None 

863 

864 This is an internal routine. 

865 """ 

866 if self._hasna: 

867 if convert: 

868 result = result.astype(convert) 

869 if fill_value is None: 

870 fill_value = np.nan 

871 np.putmask(result, self._isnan, fill_value) 

872 return result 

873 

874 # ------------------------------------------------------------------ 

875 # Frequency Properties/Methods 

876 

877 @property 

878 def freqstr(self) -> str | None: 

879 """ 

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

881 

882 See Also 

883 -------- 

884 DatetimeIndex.inferred_freq : Returns a string representing a frequency 

885 generated by infer_freq. 

886 

887 Examples 

888 -------- 

889 For DatetimeIndex: 

890 

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

892 >>> idx.freqstr 

893 'D' 

894 

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

896 

897 >>> idx = pd.DatetimeIndex( 

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

899 ... ) 

900 >>> idx.freqstr 

901 '2D' 

902 

903 For PeriodIndex: 

904 

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

906 >>> idx.freqstr 

907 'M' 

908 """ 

909 if self.freq is None: 

910 return None 

911 return self.freq.freqstr 

912 

913 @property # NB: override with cache_readonly in immutable subclasses 

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

915 """ 

916 Tries to return a string representing a frequency generated by infer_freq. 

917 

918 Returns None if it can't autodetect the frequency. 

919 

920 See Also 

921 -------- 

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

923 otherwise None. 

924 

925 Examples 

926 -------- 

927 For DatetimeIndex: 

928 

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

930 >>> idx.inferred_freq 

931 '2D' 

932 

933 For TimedeltaIndex: 

934 

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

936 >>> tdelta_idx 

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

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

939 >>> tdelta_idx.inferred_freq 

940 '10D' 

941 """ 

942 if self.ndim != 1: 

943 return None 

944 try: 

945 return frequencies.infer_freq(self) 

946 except ValueError: 

947 return None 

948 

949 @property # NB: override with cache_readonly in immutable subclasses 

950 def _resolution_obj(self) -> Resolution | None: 

951 freqstr = self.freqstr 

952 if freqstr is None: 

953 return None 

954 try: 

955 return Resolution.get_reso_from_freqstr(freqstr) 

956 except KeyError: 

957 return None 

958 

959 @property # NB: override with cache_readonly in immutable subclasses 

960 def resolution(self) -> str: 

961 """ 

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

963 """ 

964 # error: Item "None" of "Optional[Any]" has no attribute "attrname" 

965 return self._resolution_obj.attrname # type: ignore[union-attr] 

966 

967 # monotonicity/uniqueness properties are called via frequencies.infer_freq, 

968 # see GH#23789 

969 

970 @property 

971 def _is_monotonic_increasing(self) -> bool: 

972 return algos.is_monotonic(self.asi8, timelike=True)[0] 

973 

974 @property 

975 def _is_monotonic_decreasing(self) -> bool: 

976 return algos.is_monotonic(self.asi8, timelike=True)[1] 

977 

978 @property 

979 def _is_unique(self) -> bool: 

980 return len(unique1d(self.asi8.ravel("K"))) == self.size 

981 

982 # ------------------------------------------------------------------ 

983 # Arithmetic Methods 

984 

985 def _cmp_method(self, other, op): 

986 if self.ndim > 1 and getattr(other, "shape", None) == self.shape: 

987 # TODO: handle 2D-like listlikes 

988 return op(self.ravel(), other.ravel()).reshape(self.shape) 

989 

990 try: 

991 other = self._validate_comparison_value(other) 

992 except InvalidComparison: 

993 if hasattr(other, "dtype") and isinstance(other.dtype, ArrowDtype): 

994 return NotImplemented 

995 return invalid_comparison(self, other, op) 

996 

997 dtype = getattr(other, "dtype", None) 

998 if is_object_dtype(dtype): 

999 # We have to use comp_method_OBJECT_ARRAY instead of numpy 

1000 # comparison otherwise it would raise when comparing to None 

1001 result = ops.comp_method_OBJECT_ARRAY( 

1002 op, np.asarray(self.astype(object)), other 

1003 ) 

1004 return result 

1005 if other is NaT: 

1006 if op is operator.ne: 

1007 result = np.ones(self.shape, dtype=bool) 

1008 else: 

1009 result = np.zeros(self.shape, dtype=bool) 

1010 return result 

1011 

1012 if not isinstance(self.dtype, PeriodDtype): 

1013 self = cast(TimelikeOps, self) 

1014 if self._creso != other._creso: 

1015 if not isinstance(other, type(self)): 

1016 # i.e. Timedelta/Timestamp, cast to ndarray and let 

1017 # compare_mismatched_resolutions handle broadcasting 

1018 try: 

1019 # GH#52080 see if we can losslessly cast to shared unit 

1020 other = other.as_unit(self.unit, round_ok=False) 

1021 except ValueError: 

1022 other_arr = np.array(other.asm8) 

1023 return compare_mismatched_resolutions( 

1024 self._ndarray, other_arr, op 

1025 ) 

1026 else: 

1027 other_arr = other._ndarray 

1028 return compare_mismatched_resolutions(self._ndarray, other_arr, op) 

1029 

1030 other_vals = self._unbox(other) 

1031 # GH#37462 comparison on i8 values is almost 2x faster than M8/m8 

1032 result = op(self._ndarray.view("i8"), other_vals.view("i8")) 

1033 

1034 o_mask = isna(other) 

1035 mask = self._isnan | o_mask 

1036 if mask.any(): 

1037 nat_result = op is operator.ne 

1038 np.putmask(result, mask, nat_result) 

1039 

1040 return result 

1041 

1042 # pow is invalid for all three subclasses; TimedeltaArray will override 

1043 # the multiplication and division ops 

1044 __pow__ = _make_unpacked_invalid_op("__pow__") 

1045 __rpow__ = _make_unpacked_invalid_op("__rpow__") 

1046 __mul__ = _make_unpacked_invalid_op("__mul__") 

1047 __rmul__ = _make_unpacked_invalid_op("__rmul__") 

1048 __truediv__ = _make_unpacked_invalid_op("__truediv__") 

1049 __rtruediv__ = _make_unpacked_invalid_op("__rtruediv__") 

1050 __floordiv__ = _make_unpacked_invalid_op("__floordiv__") 

1051 __rfloordiv__ = _make_unpacked_invalid_op("__rfloordiv__") 

1052 __mod__ = _make_unpacked_invalid_op("__mod__") 

1053 __rmod__ = _make_unpacked_invalid_op("__rmod__") 

1054 __divmod__ = _make_unpacked_invalid_op("__divmod__") 

1055 __rdivmod__ = _make_unpacked_invalid_op("__rdivmod__") 

1056 

1057 @final 

1058 def _get_i8_values_and_mask( 

1059 self, other 

1060 ) -> tuple[int | npt.NDArray[np.int64], None | npt.NDArray[np.bool_]]: 

1061 """ 

1062 Get the int64 values and b_mask to pass to add_overflowsafe. 

1063 """ 

1064 if isinstance(other, Period): 

1065 i8values = other.ordinal 

1066 mask = None 

1067 elif isinstance(other, (Timestamp, Timedelta)): 

1068 i8values = other._value 

1069 mask = None 

1070 else: 

1071 # PeriodArray, DatetimeArray, TimedeltaArray 

1072 mask = other._isnan 

1073 i8values = other.asi8 

1074 return i8values, mask 

1075 

1076 @final 

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

1078 """ 

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

1080 """ 

1081 # Adding or subtracting a Timedelta/Timestamp scalar is freq-preserving 

1082 # whenever self.freq is a Tick 

1083 if isinstance(self.dtype, PeriodDtype): 

1084 return self.freq 

1085 elif not lib.is_scalar(other): 

1086 return None 

1087 elif isinstance(self.freq, Tick): 

1088 # In these cases 

1089 return self.freq 

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

1091 return self.freq 

1092 elif ( 

1093 self.dtype.kind == "m" 

1094 and isinstance(other, Timestamp) 

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

1096 ): 

1097 # e.g. test_td64arr_add_sub_datetimelike_scalar tdarr + timestamp 

1098 # gives a DatetimeArray. As long as the timestamp has no timezone 

1099 # or UTC, the result can retain a Day freq. 

1100 return self.freq 

1101 elif ( 

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

1103 and isinstance(self.freq, Day) 

1104 and isinstance(other, Timedelta) 

1105 ): 

1106 # e.g. TestTimedelta64ArithmeticUnsorted::test_timedelta 

1107 # Day is unambiguously 24h 

1108 return self.freq 

1109 elif ( 

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

1111 and isinstance(other, Timestamp) 

1112 and isinstance(self.freq, Day) 

1113 ): 

1114 return self.freq 

1115 

1116 return None 

1117 

1118 @final 

1119 def _add_datetimelike_scalar(self, other) -> DatetimeArray: 

1120 if not lib.is_np_dtype(self.dtype, "m"): 

1121 raise TypeError( 

1122 f"cannot add {type(self).__name__} and {type(other).__name__}" 

1123 ) 

1124 

1125 self = cast("TimedeltaArray", self) 

1126 

1127 from pandas.core.arrays import DatetimeArray 

1128 from pandas.core.arrays.datetimes import tz_to_dtype 

1129 

1130 assert other is not NaT 

1131 if isna(other): 

1132 # i.e. np.datetime64("NaT") 

1133 # In this case we specifically interpret NaT as a datetime, not 

1134 # the timedelta interpretation we would get by returning self + NaT 

1135 result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self.unit}]") 

1136 # Preserve our resolution 

1137 return DatetimeArray._simple_new(result, dtype=result.dtype) 

1138 

1139 other = Timestamp(other) 

1140 self, other = self._ensure_matching_resos(other) 

1141 self = cast("TimedeltaArray", self) 

1142 

1143 other_i8, o_mask = self._get_i8_values_and_mask(other) 

1144 result = add_overflowsafe(self.asi8, np.asarray(other_i8, dtype="i8")) 

1145 res_values = result.view(f"M8[{self.unit}]") 

1146 

1147 dtype = tz_to_dtype(tz=other.tz, unit=self.unit) 

1148 res_values = result.view(f"M8[{self.unit}]") 

1149 new_freq = self._get_arithmetic_result_freq(other) 

1150 return DatetimeArray._simple_new(res_values, dtype=dtype, freq=new_freq) 

1151 

1152 @final 

1153 def _add_datetime_arraylike(self, other: DatetimeArray) -> DatetimeArray: 

1154 if not lib.is_np_dtype(self.dtype, "m"): 

1155 raise TypeError( 

1156 f"cannot add {type(self).__name__} and {type(other).__name__}" 

1157 ) 

1158 

1159 # defer to DatetimeArray.__add__ 

1160 return other + self 

1161 

1162 @final 

1163 def _sub_datetimelike_scalar( 

1164 self, other: datetime | np.datetime64 

1165 ) -> TimedeltaArray: 

1166 if self.dtype.kind != "M": 

1167 raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}") 

1168 

1169 self = cast("DatetimeArray", self) 

1170 # subtract a datetime from myself, yielding an ndarray[timedelta64[ns]] 

1171 

1172 if isna(other): 

1173 # i.e. np.datetime64("NaT") 

1174 return self - NaT 

1175 

1176 ts = Timestamp(other) 

1177 

1178 self, ts = self._ensure_matching_resos(ts) 

1179 return self._sub_datetimelike(ts) 

1180 

1181 @final 

1182 def _sub_datetime_arraylike(self, other: DatetimeArray) -> TimedeltaArray: 

1183 if self.dtype.kind != "M": 

1184 raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}") 

1185 

1186 if len(self) != len(other): 

1187 raise ValueError("cannot add indices of unequal length") 

1188 

1189 self = cast("DatetimeArray", self) 

1190 

1191 self, other = self._ensure_matching_resos(other) 

1192 return self._sub_datetimelike(other) 

1193 

1194 @final 

1195 def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray: 

1196 self = cast("DatetimeArray", self) 

1197 

1198 from pandas.core.arrays import TimedeltaArray 

1199 

1200 try: 

1201 self._assert_tzawareness_compat(other) 

1202 except TypeError as err: 

1203 new_message = str(err).replace("compare", "subtract") 

1204 raise type(err)(new_message) from err 

1205 

1206 other_i8, o_mask = self._get_i8_values_and_mask(other) 

1207 res_values = add_overflowsafe(self.asi8, np.asarray(-other_i8, dtype="i8")) 

1208 res_m8 = res_values.view(f"timedelta64[{self.unit}]") 

1209 

1210 new_freq = self._get_arithmetic_result_freq(other) 

1211 new_freq = cast("Tick | None", new_freq) 

1212 return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype, freq=new_freq) 

1213 

1214 @final 

1215 def _add_period(self, other: Period) -> PeriodArray: 

1216 if not lib.is_np_dtype(self.dtype, "m"): 

1217 raise TypeError(f"cannot add Period to a {type(self).__name__}") 

1218 

1219 # We will wrap in a PeriodArray and defer to the reversed operation 

1220 from pandas.core.arrays.period import PeriodArray 

1221 

1222 i8vals = np.broadcast_to(other.ordinal, self.shape) 

1223 dtype = PeriodDtype(other.freq) 

1224 parr = PeriodArray(i8vals, dtype=dtype) 

1225 return parr + self 

1226 

1227 def _add_offset(self, offset): 

1228 raise AbstractMethodError(self) 

1229 

1230 def _add_timedeltalike_scalar(self, other): 

1231 """ 

1232 Add a delta of a timedeltalike 

1233 

1234 Returns 

1235 ------- 

1236 Same type as self 

1237 """ 

1238 if isna(other): 

1239 # i.e np.timedelta64("NaT") 

1240 new_values = np.empty(self.shape, dtype="i8").view(self._ndarray.dtype) 

1241 new_values.fill(iNaT) 

1242 return type(self)._simple_new(new_values, dtype=self.dtype) 

1243 

1244 # PeriodArray overrides, so we only get here with DTA/TDA 

1245 self = cast("DatetimeArray | TimedeltaArray", self) 

1246 other = Timedelta(other) 

1247 self, other = self._ensure_matching_resos(other) 

1248 return self._add_timedeltalike(other) 

1249 

1250 def _add_timedelta_arraylike(self, other: TimedeltaArray) -> Self: 

1251 """ 

1252 Add a delta of a TimedeltaIndex 

1253 

1254 Returns 

1255 ------- 

1256 Same type as self 

1257 """ 

1258 # overridden by PeriodArray 

1259 

1260 if len(self) != len(other): 

1261 raise ValueError("cannot add indices of unequal length") 

1262 

1263 self, other = cast( 

1264 "DatetimeArray | TimedeltaArray", self 

1265 )._ensure_matching_resos(other) 

1266 return self._add_timedeltalike(other) 

1267 

1268 @final 

1269 def _add_timedeltalike(self, other: Timedelta | TimedeltaArray) -> Self: 

1270 other_i8, o_mask = self._get_i8_values_and_mask(other) 

1271 new_values = add_overflowsafe(self.asi8, np.asarray(other_i8, dtype="i8")) 

1272 res_values = new_values.view(self._ndarray.dtype) 

1273 

1274 new_freq = self._get_arithmetic_result_freq(other) 

1275 

1276 # error: Unexpected keyword argument "freq" for "_simple_new" of "NDArrayBacked" 

1277 return type(self)._simple_new( 

1278 res_values, 

1279 dtype=self.dtype, 

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

1281 ) 

1282 

1283 @final 

1284 def _add_nat(self) -> Self: 

1285 """ 

1286 Add pd.NaT to self 

1287 """ 

1288 if isinstance(self.dtype, PeriodDtype): 

1289 raise TypeError( 

1290 f"Cannot add {type(self).__name__} and {type(NaT).__name__}" 

1291 ) 

1292 

1293 # GH#19124 pd.NaT is treated like a timedelta for both timedelta 

1294 # and datetime dtypes 

1295 result = np.empty(self.shape, dtype=np.int64) 

1296 result.fill(iNaT) 

1297 result = result.view(self._ndarray.dtype) # preserve reso 

1298 # error: Unexpected keyword argument "freq" for "_simple_new" of "NDArrayBacked" 

1299 return type(self)._simple_new( 

1300 result, 

1301 dtype=self.dtype, 

1302 freq=None, # type: ignore[call-arg] 

1303 ) 

1304 

1305 @final 

1306 def _sub_nat(self) -> np.ndarray: 

1307 """ 

1308 Subtract pd.NaT from self 

1309 """ 

1310 # GH#19124 Timedelta - datetime is not in general well-defined. 

1311 # We make an exception for pd.NaT, which in this case quacks 

1312 # like a timedelta. 

1313 # For datetime64 dtypes by convention we treat NaT as a datetime, so 

1314 # this subtraction returns a timedelta64 dtype. 

1315 # For period dtype, timedelta64 is a close-enough return dtype. 

1316 result = np.empty(self.shape, dtype=np.int64) 

1317 result.fill(iNaT) 

1318 if self.dtype.kind in "mM": 

1319 # We can retain unit in dtype 

1320 self = cast("DatetimeArray| TimedeltaArray", self) 

1321 return result.view(f"timedelta64[{self.unit}]") 

1322 else: 

1323 return result.view("timedelta64[ns]") 

1324 

1325 @final 

1326 def _sub_periodlike(self, other: Period | PeriodArray) -> npt.NDArray[np.object_]: 

1327 # If the operation is well-defined, we return an object-dtype ndarray 

1328 # of DateOffsets. Null entries are filled with pd.NaT 

1329 if not isinstance(self.dtype, PeriodDtype): 

1330 raise TypeError( 

1331 f"cannot subtract {type(other).__name__} from {type(self).__name__}" 

1332 ) 

1333 

1334 self = cast("PeriodArray", self) 

1335 self._check_compatible_with(other) 

1336 

1337 other_i8, o_mask = self._get_i8_values_and_mask(other) 

1338 new_i8_data = add_overflowsafe(self.asi8, np.asarray(-other_i8, dtype="i8")) 

1339 new_data = np.array([self.freq.base * x for x in new_i8_data]) 

1340 

1341 if o_mask is None: 

1342 # i.e. Period scalar 

1343 mask = self._isnan 

1344 else: 

1345 # i.e. PeriodArray 

1346 mask = self._isnan | o_mask 

1347 new_data[mask] = NaT 

1348 return new_data 

1349 

1350 @final 

1351 def _addsub_object_array(self, other: npt.NDArray[np.object_], op) -> np.ndarray: 

1352 """ 

1353 Add or subtract array-like of DateOffset objects 

1354 

1355 Parameters 

1356 ---------- 

1357 other : np.ndarray[object] 

1358 op : {operator.add, operator.sub} 

1359 

1360 Returns 

1361 ------- 

1362 np.ndarray[object] 

1363 Except in fastpath case with length 1 where we operate on the 

1364 contained scalar. 

1365 """ 

1366 assert op in [operator.add, operator.sub] 

1367 if len(other) == 1 and self.ndim == 1: 

1368 # Note: without this special case, we could annotate return type 

1369 # as ndarray[object] 

1370 # If both 1D then broadcasting is unambiguous 

1371 return op(self, other[0]) 

1372 

1373 if get_option("performance_warnings"): 

1374 warnings.warn( 

1375 "Adding/subtracting object-dtype array to " 

1376 f"{type(self).__name__} not vectorized.", 

1377 PerformanceWarning, 

1378 stacklevel=find_stack_level(), 

1379 ) 

1380 

1381 # Caller is responsible for broadcasting if necessary 

1382 assert self.shape == other.shape, (self.shape, other.shape) 

1383 

1384 res_values = op(self.astype("O"), np.asarray(other)) 

1385 return res_values 

1386 

1387 def _accumulate(self, name: str, *, skipna: bool = True, **kwargs) -> Self: 

1388 if name not in {"cummin", "cummax"}: 

1389 raise TypeError(f"Accumulation {name} not supported for {type(self)}") 

1390 

1391 op = getattr(datetimelike_accumulations, name) 

1392 result = op(self.copy(), skipna=skipna, **kwargs) 

1393 

1394 return type(self)._simple_new(result, dtype=self.dtype) 

1395 

1396 @unpack_zerodim_and_defer("__add__") 

1397 def __add__(self, other): 

1398 other_dtype = getattr(other, "dtype", None) 

1399 other = ensure_wrapped_if_datetimelike(other) 

1400 

1401 # scalar others 

1402 if other is NaT: 

1403 result: np.ndarray | DatetimeLikeArrayMixin = self._add_nat() 

1404 elif isinstance(other, (Tick, timedelta, np.timedelta64)): 

1405 result = self._add_timedeltalike_scalar(other) 

1406 elif isinstance(other, Day) and lib.is_np_dtype(self.dtype, "Mm"): 

1407 # We treat this as Tick-like 

1408 td = Timedelta(days=other.n).as_unit("s") 

1409 result = self._add_timedeltalike_scalar(td) 

1410 elif isinstance(other, BaseOffset): 

1411 # specifically _not_ a Tick 

1412 result = self._add_offset(other) 

1413 elif isinstance(other, (datetime, np.datetime64)): 

1414 result = self._add_datetimelike_scalar(other) 

1415 elif isinstance(other, Period) and lib.is_np_dtype(self.dtype, "m"): 

1416 result = self._add_period(other) 

1417 elif lib.is_integer(other): 

1418 # This check must come after the check for np.timedelta64 

1419 # as is_integer returns True for these 

1420 if not isinstance(self.dtype, PeriodDtype): 

1421 raise integer_op_not_supported(self) 

1422 obj = cast("PeriodArray", self) 

1423 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.add) 

1424 

1425 # array-like others 

1426 elif lib.is_np_dtype(other_dtype, "m"): 

1427 # TimedeltaIndex, ndarray[timedelta64] 

1428 result = self._add_timedelta_arraylike(other) 

1429 elif is_object_dtype(other_dtype): 

1430 # e.g. Array/Index of DateOffset objects 

1431 result = self._addsub_object_array(other, operator.add) 

1432 elif lib.is_np_dtype(other_dtype, "M") or isinstance( 

1433 other_dtype, DatetimeTZDtype 

1434 ): 

1435 # DatetimeIndex, ndarray[datetime64] 

1436 return self._add_datetime_arraylike(other) 

1437 elif is_integer_dtype(other_dtype): 

1438 if not isinstance(self.dtype, PeriodDtype): 

1439 raise integer_op_not_supported(self) 

1440 obj = cast("PeriodArray", self) 

1441 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.add) 

1442 else: 

1443 # Includes Categorical, other ExtensionArrays 

1444 # For PeriodDtype, if self is a TimedeltaArray and other is a 

1445 # PeriodArray with a timedelta-like (i.e. Tick) freq, this 

1446 # operation is valid. Defer to the PeriodArray implementation. 

1447 # In remaining cases, this will end up raising TypeError. 

1448 return NotImplemented 

1449 

1450 if isinstance(result, np.ndarray) and lib.is_np_dtype(result.dtype, "m"): 

1451 from pandas.core.arrays import TimedeltaArray 

1452 

1453 return TimedeltaArray._from_sequence(result, dtype=result.dtype) 

1454 return result 

1455 

1456 def __radd__(self, other): 

1457 # alias for __add__ 

1458 return self.__add__(other) 

1459 

1460 @unpack_zerodim_and_defer("__sub__") 

1461 def __sub__(self, other): 

1462 other_dtype = getattr(other, "dtype", None) 

1463 other = ensure_wrapped_if_datetimelike(other) 

1464 

1465 # scalar others 

1466 if other is NaT: 

1467 result: np.ndarray | DatetimeLikeArrayMixin = self._sub_nat() 

1468 elif isinstance(other, (Tick, timedelta, np.timedelta64)): 

1469 result = self._add_timedeltalike_scalar(-other) 

1470 elif isinstance(other, Day) and lib.is_np_dtype(self.dtype, "Mm"): 

1471 # We treat this as Tick-like 

1472 td = Timedelta(days=other.n).as_unit("s") 

1473 result = self._add_timedeltalike_scalar(-td) 

1474 elif isinstance(other, BaseOffset): 

1475 # specifically _not_ a Tick 

1476 result = self._add_offset(-other) 

1477 elif isinstance(other, (datetime, np.datetime64)): 

1478 result = self._sub_datetimelike_scalar(other) 

1479 elif lib.is_integer(other): 

1480 # This check must come after the check for np.timedelta64 

1481 # as is_integer returns True for these 

1482 if not isinstance(self.dtype, PeriodDtype): 

1483 raise integer_op_not_supported(self) 

1484 obj = cast("PeriodArray", self) 

1485 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.sub) 

1486 

1487 elif isinstance(other, Period): 

1488 result = self._sub_periodlike(other) 

1489 

1490 # array-like others 

1491 elif lib.is_np_dtype(other_dtype, "m"): 

1492 # TimedeltaIndex, ndarray[timedelta64] 

1493 result = self._add_timedelta_arraylike(-other) 

1494 elif is_object_dtype(other_dtype): 

1495 # e.g. Array/Index of DateOffset objects 

1496 result = self._addsub_object_array(other, operator.sub) 

1497 elif lib.is_np_dtype(other_dtype, "M") or isinstance( 

1498 other_dtype, DatetimeTZDtype 

1499 ): 

1500 # DatetimeIndex, ndarray[datetime64] 

1501 result = self._sub_datetime_arraylike(other) 

1502 elif isinstance(other_dtype, PeriodDtype): 

1503 # PeriodIndex 

1504 result = self._sub_periodlike(other) 

1505 elif is_integer_dtype(other_dtype): 

1506 if not isinstance(self.dtype, PeriodDtype): 

1507 raise integer_op_not_supported(self) 

1508 obj = cast("PeriodArray", self) 

1509 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.sub) 

1510 else: 

1511 # Includes ExtensionArrays, float_dtype 

1512 return NotImplemented 

1513 

1514 if isinstance(result, np.ndarray) and lib.is_np_dtype(result.dtype, "m"): 

1515 from pandas.core.arrays import TimedeltaArray 

1516 

1517 return TimedeltaArray._from_sequence(result, dtype=result.dtype) 

1518 return result 

1519 

1520 def __rsub__(self, other): 

1521 other_dtype = getattr(other, "dtype", None) 

1522 other_is_dt64 = lib.is_np_dtype(other_dtype, "M") or isinstance( 

1523 other_dtype, DatetimeTZDtype 

1524 ) 

1525 

1526 if other_is_dt64 and lib.is_np_dtype(self.dtype, "m"): 

1527 # ndarray[datetime64] cannot be subtracted from self, so 

1528 # we need to wrap in DatetimeArray/Index and flip the operation 

1529 if lib.is_scalar(other): 

1530 # i.e. np.datetime64 object 

1531 return Timestamp(other) - self 

1532 if not isinstance(other, DatetimeLikeArrayMixin): 

1533 # Avoid down-casting DatetimeIndex 

1534 from pandas.core.arrays import DatetimeArray 

1535 

1536 other = DatetimeArray._from_sequence(other, dtype=other.dtype) 

1537 return other - self 

1538 elif self.dtype.kind == "M" and hasattr(other, "dtype") and not other_is_dt64: 

1539 # GH#19959 datetime - datetime is well-defined as timedelta, 

1540 # but any other type - datetime is not well-defined. 

1541 raise TypeError( 

1542 f"cannot subtract {type(self).__name__} from " 

1543 f"{type(other).__name__}[{other.dtype}]" 

1544 ) 

1545 elif isinstance(self.dtype, PeriodDtype) and lib.is_np_dtype(other_dtype, "m"): 

1546 # TODO: Can we simplify/generalize these cases at all? 

1547 raise TypeError(f"cannot subtract {type(self).__name__} from {other.dtype}") 

1548 elif lib.is_np_dtype(self.dtype, "m"): 

1549 self = cast("TimedeltaArray", self) 

1550 return (-self) + other 

1551 

1552 flipped = self - other 

1553 if flipped.dtype.kind == "M": 

1554 # GH#59571 give a more helpful exception message 

1555 raise TypeError( 

1556 f"cannot subtract {type(self).__name__} from {type(other).__name__}" 

1557 ) 

1558 # We get here with e.g. datetime objects 

1559 return -flipped 

1560 

1561 def __iadd__(self, other) -> Self: 

1562 result = self + other 

1563 self[:] = result[:] 

1564 

1565 if not isinstance(self.dtype, PeriodDtype): 

1566 # restore freq, which is invalidated by setitem 

1567 self._freq = result.freq 

1568 return self 

1569 

1570 def __isub__(self, other) -> Self: 

1571 result = self - other 

1572 self[:] = result[:] 

1573 

1574 if not isinstance(self.dtype, PeriodDtype): 

1575 # restore freq, which is invalidated by setitem 

1576 self._freq = result.freq 

1577 return self 

1578 

1579 # -------------------------------------------------------------- 

1580 # Reductions 

1581 

1582 @_period_dispatch 

1583 def _quantile( 

1584 self, 

1585 qs: npt.NDArray[np.float64], 

1586 interpolation: str, 

1587 ) -> Self: 

1588 return super()._quantile(qs=qs, interpolation=interpolation) 

1589 

1590 @_period_dispatch 

1591 def min(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs): 

1592 """ 

1593 Return the minimum value of the Array or minimum along 

1594 an axis. 

1595 

1596 See Also 

1597 -------- 

1598 numpy.ndarray.min 

1599 Index.min : Return the minimum value in an Index. 

1600 Series.min : Return the minimum value in a Series. 

1601 """ 

1602 nv.validate_min((), kwargs) 

1603 nv.validate_minmax_axis(axis, self.ndim) 

1604 

1605 result = nanops.nanmin(self._ndarray, axis=axis, skipna=skipna) 

1606 return self._wrap_reduction_result(axis, result) 

1607 

1608 @_period_dispatch 

1609 def max(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs): 

1610 """ 

1611 Return the maximum value of the Array or maximum along 

1612 an axis. 

1613 

1614 See Also 

1615 -------- 

1616 numpy.ndarray.max 

1617 Index.max : Return the maximum value in an Index. 

1618 Series.max : Return the maximum value in a Series. 

1619 """ 

1620 nv.validate_max((), kwargs) 

1621 nv.validate_minmax_axis(axis, self.ndim) 

1622 

1623 result = nanops.nanmax(self._ndarray, axis=axis, skipna=skipna) 

1624 return self._wrap_reduction_result(axis, result) 

1625 

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

1627 """ 

1628 Return the mean value of the Array. 

1629 

1630 Parameters 

1631 ---------- 

1632 skipna : bool, default True 

1633 Whether to ignore any NaT elements. 

1634 axis : int, optional, default 0 

1635 Axis for the function to be applied on. 

1636 

1637 Returns 

1638 ------- 

1639 scalar 

1640 Timestamp or Timedelta. 

1641 

1642 See Also 

1643 -------- 

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

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

1646 

1647 Notes 

1648 ----- 

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

1650 

1651 Examples 

1652 -------- 

1653 For :class:`pandas.DatetimeIndex`: 

1654 

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

1656 >>> idx 

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

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

1659 >>> idx.mean() 

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

1661 

1662 For :class:`pandas.TimedeltaIndex`: 

1663 

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

1665 >>> tdelta_idx 

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

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

1668 >>> tdelta_idx.mean() 

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

1670 """ 

1671 if isinstance(self.dtype, PeriodDtype): 

1672 # See discussion in GH#24757 

1673 raise TypeError( 

1674 f"mean is not implemented for {type(self).__name__} since the " 

1675 "meaning is ambiguous. An alternative is " 

1676 "obj.to_timestamp(how='start').mean()" 

1677 ) 

1678 

1679 result = nanops.nanmean( 

1680 self._ndarray, axis=axis, skipna=skipna, mask=self.isna() 

1681 ) 

1682 return self._wrap_reduction_result(axis, result) 

1683 

1684 @_period_dispatch 

1685 def median(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs): 

1686 nv.validate_median((), kwargs) 

1687 

1688 if axis is not None and abs(axis) >= self.ndim: 

1689 raise ValueError("abs(axis) must be less than ndim") 

1690 

1691 result = nanops.nanmedian(self._ndarray, axis=axis, skipna=skipna) 

1692 return self._wrap_reduction_result(axis, result) 

1693 

1694 def _mode(self, dropna: bool = True): 

1695 mask = None 

1696 if dropna: 

1697 mask = self.isna() 

1698 

1699 i8modes, _ = algorithms.mode(self.view("i8"), mask=mask) 

1700 npmodes = i8modes.view(self._ndarray.dtype) 

1701 npmodes = cast(np.ndarray, npmodes) 

1702 return self._from_backing_data(npmodes) 

1703 

1704 # ------------------------------------------------------------------ 

1705 # GroupBy Methods 

1706 

1707 def _groupby_op( 

1708 self, 

1709 *, 

1710 how: str, 

1711 has_dropped_na: bool, 

1712 min_count: int, 

1713 ngroups: int, 

1714 ids: npt.NDArray[np.intp], 

1715 **kwargs, 

1716 ): 

1717 dtype = self.dtype 

1718 if dtype.kind == "M": 

1719 # Adding/multiplying datetimes is not valid 

1720 if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]: 

1721 raise TypeError(f"datetime64 type does not support operation '{how}'") 

1722 if how in ["any", "all"]: 

1723 # GH#34479 

1724 raise TypeError( 

1725 f"'{how}' with datetime64 dtypes is no longer supported. " 

1726 f"Use (obj != pd.Timestamp(0)).{how}() instead." 

1727 ) 

1728 

1729 elif isinstance(dtype, PeriodDtype): 

1730 # Adding/multiplying Periods is not valid 

1731 if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]: 

1732 raise TypeError(f"Period type does not support {how} operations") 

1733 if how in ["any", "all"]: 

1734 # GH#34479 

1735 raise TypeError( 

1736 f"'{how}' with PeriodDtype is no longer supported. " 

1737 f"Use (obj != pd.Period(0, freq)).{how}() instead." 

1738 ) 

1739 # timedeltas we can add but not multiply 

1740 elif how in ["prod", "cumprod", "skew", "kurt", "var"]: 

1741 raise TypeError(f"timedelta64 type does not support {how} operations") 

1742 

1743 # All of the functions implemented here are ordinal, so we can 

1744 # operate on the tz-naive equivalents 

1745 npvalues = self._ndarray.view("M8[ns]") 

1746 

1747 from pandas.core.groupby.ops import WrappedCythonOp 

1748 

1749 kind = WrappedCythonOp.get_kind_from_how(how) 

1750 op = WrappedCythonOp(how=how, kind=kind, has_dropped_na=has_dropped_na) 

1751 

1752 res_values = op._cython_op_ndim_compat( 

1753 npvalues, 

1754 min_count=min_count, 

1755 ngroups=ngroups, 

1756 comp_ids=ids, 

1757 mask=None, 

1758 **kwargs, 

1759 ) 

1760 

1761 if op.how in op.cast_blocklist: 

1762 # i.e. how in ["rank"], since other cast_blocklist methods don't go 

1763 # through cython_operation 

1764 return res_values 

1765 

1766 # We did a view to M8[ns] above, now we go the other direction 

1767 assert res_values.dtype == "M8[ns]" 

1768 if how in ["std", "sem"]: 

1769 from pandas.core.arrays import TimedeltaArray 

1770 

1771 if isinstance(self.dtype, PeriodDtype): 

1772 raise TypeError("'std' and 'sem' are not valid for PeriodDtype") 

1773 self = cast("DatetimeArray | TimedeltaArray", self) 

1774 new_dtype = f"m8[{self.unit}]" 

1775 res_values = res_values.view(new_dtype) 

1776 return TimedeltaArray._simple_new(res_values, dtype=res_values.dtype) 

1777 

1778 res_values = res_values.view(self._ndarray.dtype) 

1779 return self._from_backing_data(res_values) 

1780 

1781 

1782class DatelikeOps(DatetimeLikeArrayMixin): 

1783 """ 

1784 Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex. 

1785 """ 

1786 

1787 def strftime(self, date_format: str) -> npt.NDArray[np.object_]: 

1788 """ 

1789 Convert to Index using specified date_format. 

1790 

1791 Return an Index of formatted strings specified by date_format, which 

1792 supports the same string format as the python standard library. Details 

1793 of the string format can be found in `python string format 

1794 doc <https://docs.python.org/3/library/datetime.html 

1795 #strftime-and-strptime-behavior>`__. 

1796 

1797 Formats supported by the C `strftime` API but not by the python string format 

1798 doc (such as `"%R"`, `"%r"`) are not officially supported and should be 

1799 preferably replaced with their supported equivalents (such as `"%H:%M"`, 

1800 `"%I:%M:%S %p"`). 

1801 

1802 Note that `PeriodIndex` support additional directives, detailed in 

1803 `Period.strftime`. 

1804 

1805 Parameters 

1806 ---------- 

1807 date_format : str 

1808 Date format string (e.g. "%%Y-%%m-%%d"). 

1809 

1810 Returns 

1811 ------- 

1812 ndarray[object] 

1813 NumPy ndarray of formatted strings. 

1814 

1815 See Also 

1816 -------- 

1817 to_datetime : Convert the given argument to datetime. 

1818 DatetimeIndex.normalize : Return DatetimeIndex with times to midnight. 

1819 DatetimeIndex.round : Round the DatetimeIndex to the specified freq. 

1820 DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq. 

1821 Timestamp.strftime : Format a single Timestamp. 

1822 Period.strftime : Format a single Period. 

1823 

1824 Examples 

1825 -------- 

1826 >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"), periods=3, freq="s") 

1827 >>> rng.strftime("%B %d, %Y, %r") 

1828 Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM', 

1829 'March 10, 2018, 09:00:02 AM'], 

1830 dtype='str') 

1831 """ 

1832 result = self._format_native_types(date_format=date_format, na_rep=np.nan) 

1833 if using_string_dtype(): 

1834 from pandas import StringDtype 

1835 

1836 return pd_array(result, dtype=StringDtype(na_value=np.nan)) # type: ignore[return-value] 

1837 return result.astype(object, copy=False) 

1838 

1839 

1840class TimelikeOps(DatetimeLikeArrayMixin): 

1841 """ 

1842 Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex. 

1843 """ 

1844 

1845 @classmethod 

1846 def _validate_dtype(cls, values, dtype): 

1847 raise AbstractMethodError(cls) 

1848 

1849 @property 

1850 def freq(self): 

1851 """ 

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

1853 

1854 To learn more about the frequency strings, please see 

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

1856 

1857 See Also 

1858 -------- 

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

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

1861 

1862 Examples 

1863 -------- 

1864 >>> datetimeindex = pd.date_range( 

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

1866 ... ) 

1867 >>> datetimeindex 

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

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

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

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

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

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

1874 >>> datetimeindex.freq 

1875 <Hour> 

1876 """ 

1877 return self._freq 

1878 

1879 @freq.setter 

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

1881 if value is not None: 

1882 value = to_offset(value) 

1883 self._validate_frequency(self, value) 

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

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

1886 

1887 if self.ndim > 1: 

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

1889 

1890 self._freq = value 

1891 

1892 @final 

1893 def _maybe_pin_freq(self, freq, validate_kwds: dict) -> None: 

1894 """ 

1895 Constructor helper to pin the appropriate `freq` attribute. Assumes 

1896 that self._freq is currently set to any freq inferred in 

1897 _from_sequence_not_strict. 

1898 """ 

1899 if freq is None: 

1900 # user explicitly passed None -> override any inferred_freq 

1901 self._freq = None 

1902 elif freq == "infer": 

1903 # if self._freq is *not* None then we already inferred a freq 

1904 # and there is nothing left to do 

1905 if self._freq is None: 

1906 # Set _freq directly to bypass duplicative _validate_frequency 

1907 # check. 

1908 self._freq = to_offset(self.inferred_freq) # type: ignore[assignment] 

1909 elif freq is lib.no_default: 

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

1911 # data had one, otherwise do nothing 

1912 pass 

1913 elif self._freq is None: 

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

1915 # the user-passed freq 

1916 freq = to_offset(freq) 

1917 type(self)._validate_frequency(self, freq, **validate_kwds) 

1918 self._freq = freq 

1919 else: 

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

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

1922 freq = to_offset(freq) 

1923 _validate_inferred_freq(freq, self._freq) 

1924 

1925 @final 

1926 @classmethod 

1927 def _validate_frequency(cls, index, freq: BaseOffset, **kwargs) -> None: 

1928 """ 

1929 Validate that a frequency is compatible with the values of a given 

1930 Datetime Array/Index or Timedelta Array/Index 

1931 

1932 Parameters 

1933 ---------- 

1934 index : DatetimeIndex or TimedeltaIndex 

1935 The index on which to determine if the given frequency is valid 

1936 freq : DateOffset 

1937 The frequency to validate 

1938 """ 

1939 inferred = index.inferred_freq 

1940 if index.size == 0 or inferred == freq.freqstr: 

1941 return None 

1942 

1943 try: 

1944 on_freq = cls._generate_range( 

1945 start=index[0], 

1946 end=None, 

1947 periods=len(index), 

1948 freq=freq, 

1949 unit=index.unit, 

1950 **kwargs, 

1951 ) 

1952 if not np.array_equal(index.asi8, on_freq.asi8): 

1953 raise ValueError 

1954 except ValueError as err: 

1955 if "non-fixed" in str(err): 

1956 # non-fixed frequencies are not meaningful for timedelta64; 

1957 # we retain that error message 

1958 raise err 

1959 # GH#11587 the main way this is reached is if the `np.array_equal` 

1960 # check above is False. This can also be reached if index[0] 

1961 # is `NaT`, in which case the call to `cls._generate_range` will 

1962 # raise a ValueError, which we re-raise with a more targeted 

1963 # message. 

1964 raise ValueError( 

1965 f"Inferred frequency {inferred} from passed values " 

1966 f"does not conform to passed frequency {freq.freqstr}" 

1967 ) from err 

1968 

1969 @classmethod 

1970 def _generate_range( 

1971 cls, start, end, periods: int | None, freq, *args, **kwargs 

1972 ) -> Self: 

1973 raise AbstractMethodError(cls) 

1974 

1975 # -------------------------------------------------------------- 

1976 

1977 @cache_readonly 

1978 def _creso(self) -> int: 

1979 return get_unit_from_dtype(self._ndarray.dtype) 

1980 

1981 @cache_readonly 

1982 def unit(self) -> TimeUnit: 

1983 """ 

1984 The precision unit of the datetime data. 

1985 

1986 Returns the precision unit for the dtype. 

1987 It means the smallest time frame that can be stored within this dtype. 

1988 

1989 Returns 

1990 ------- 

1991 str 

1992 Unit string representation (e.g. "ns"). 

1993 

1994 See Also 

1995 -------- 

1996 TimelikeOps.as_unit : Converts to a specific unit. 

1997 

1998 Examples 

1999 -------- 

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

2001 >>> idx.unit 

2002 'ns' 

2003 >>> idx.as_unit("s").unit 

2004 's' 

2005 """ 

2006 # error: Incompatible return value type (got "str", expected 

2007 # "Literal['s', 'ms', 'us', 'ns']") [return-value] 

2008 return dtype_to_unit(self.dtype) # type: ignore[return-value,arg-type] 

2009 

2010 def as_unit(self, unit: TimeUnit, round_ok: bool = True) -> Self: 

2011 """ 

2012 Convert to a dtype with the given unit resolution. 

2013 

2014 The limits of timestamp representation depend on the chosen resolution. 

2015 Different resolutions can be converted to each other through as_unit. 

2016 

2017 Parameters 

2018 ---------- 

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

2020 round_ok : bool, default True 

2021 If False and the conversion requires rounding, raise ValueError. 

2022 

2023 Returns 

2024 ------- 

2025 same type as self 

2026 Converted to the specified unit. 

2027 

2028 See Also 

2029 -------- 

2030 Timestamp.as_unit : Convert to the given unit. 

2031 

2032 Examples 

2033 -------- 

2034 For :class:`pandas.DatetimeIndex`: 

2035 

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

2037 >>> idx 

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

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

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

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

2042 

2043 For :class:`pandas.TimedeltaIndex`: 

2044 

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

2046 >>> tdelta_idx 

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

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

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

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

2051 """ 

2052 if unit not in ["s", "ms", "us", "ns"]: 

2053 raise ValueError("Supported units are 's', 'ms', 'us', 'ns'") 

2054 

2055 dtype = np.dtype(f"{self.dtype.kind}8[{unit}]") 

2056 new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=round_ok) 

2057 

2058 if isinstance(self.dtype, np.dtype): 

2059 new_dtype = new_values.dtype 

2060 else: 

2061 tz = cast("DatetimeArray", self).tz 

2062 new_dtype = DatetimeTZDtype(tz=tz, unit=unit) 

2063 

2064 # error: Unexpected keyword argument "freq" for "_simple_new" of 

2065 # "NDArrayBacked" [call-arg] 

2066 return type(self)._simple_new( 

2067 new_values, 

2068 dtype=new_dtype, 

2069 freq=self.freq, # type: ignore[call-arg] 

2070 ) 

2071 

2072 # TODO: annotate other as DatetimeArray | TimedeltaArray | Timestamp | Timedelta 

2073 # with the return type matching input type. TypeVar? 

2074 def _ensure_matching_resos(self, other): 

2075 if self._creso != other._creso: 

2076 # Just as with Timestamp/Timedelta, we cast to the higher resolution 

2077 if self._creso < other._creso: 

2078 self = self.as_unit(other.unit) 

2079 else: 

2080 other = other.as_unit(self.unit) 

2081 return self, other 

2082 

2083 # -------------------------------------------------------------- 

2084 

2085 def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): 

2086 if ( 

2087 ufunc in [np.isnan, np.isinf, np.isfinite] 

2088 and len(inputs) == 1 

2089 and inputs[0] is self 

2090 ): 

2091 # numpy 1.18 changed isinf and isnan to not raise on dt64/td64 

2092 return getattr(ufunc, method)(self._ndarray, **kwargs) 

2093 

2094 return super().__array_ufunc__(ufunc, method, *inputs, **kwargs) 

2095 

2096 def _round(self, freq, mode, ambiguous, nonexistent): 

2097 # round the local times 

2098 if isinstance(self.dtype, DatetimeTZDtype): 

2099 # operate on naive timestamps, then convert back to aware 

2100 self = cast("DatetimeArray", self) 

2101 naive = self.tz_localize(None) 

2102 result = naive._round(freq, mode, ambiguous, nonexistent) 

2103 return result.tz_localize( 

2104 self.tz, ambiguous=ambiguous, nonexistent=nonexistent 

2105 ) 

2106 

2107 values = self.view("i8") 

2108 values = cast(np.ndarray, values) 

2109 nanos = get_unit_for_round(freq, self._creso) 

2110 if nanos == 0: 

2111 # GH 52761 

2112 return self.copy() 

2113 result_i8 = round_nsint64(values, mode, nanos) 

2114 result = self._maybe_mask_results(result_i8, fill_value=iNaT) 

2115 result = result.view(self._ndarray.dtype) 

2116 return self._simple_new(result, dtype=self.dtype) 

2117 

2118 def round( 

2119 self, 

2120 freq, 

2121 ambiguous: TimeAmbiguous = "raise", 

2122 nonexistent: TimeNonexistent = "raise", 

2123 ) -> Self: 

2124 """ 

2125 Perform round operation on the data to the specified `freq`. 

2126 

2127 Parameters 

2128 ---------- 

2129 freq : str or Offset 

2130 The frequency level to round the index to. Must be a fixed 

2131 frequency like 's' (second) not 'ME' (month end). See 

2132 :ref:`frequency aliases <timeseries.offset_aliases>` for 

2133 a list of possible `freq` values. 

2134 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' 

2135 Only relevant for DatetimeIndex: 

2136 

2137 - 'infer' will attempt to infer fall dst-transition hours based on 

2138 order 

2139 - bool-ndarray where True signifies a DST time, False designates 

2140 a non-DST time (note that this flag is only applicable for 

2141 ambiguous times) 

2142 - 'NaT' will return NaT where there are ambiguous times 

2143 - 'raise' will raise a ValueError if there are ambiguous 

2144 times. 

2145 

2146 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \ 

2147 default 'raise' 

2148 A nonexistent time does not exist in a particular timezone 

2149 where clocks moved forward due to DST. 

2150 

2151 - 'shift_forward' will shift the nonexistent time forward to the 

2152 closest existing time 

2153 - 'shift_backward' will shift the nonexistent time backward to the 

2154 closest existing time 

2155 - 'NaT' will return NaT where there are nonexistent times 

2156 - timedelta objects will shift nonexistent times by the timedelta 

2157 - 'raise' will raise a ValueError if there are 

2158 nonexistent times. 

2159 

2160 Returns 

2161 ------- 

2162 DatetimeIndex, TimedeltaIndex, or Series 

2163 Index of the same type for a DatetimeIndex or TimedeltaIndex, 

2164 or a Series with the same index for a Series. 

2165 

2166 Raises 

2167 ------ 

2168 ValueError if the `freq` cannot be converted. 

2169 

2170 See Also 

2171 -------- 

2172 DatetimeIndex.floor : 

2173 Perform floor operation on the data to the specified `freq`. 

2174 DatetimeIndex.snap : 

2175 Snap time stamps to nearest occurring frequency. 

2176 

2177 Notes 

2178 ----- 

2179 If the timestamps have a timezone, rounding will take place relative to the 

2180 local ("wall") time and re-localized to the same timezone. When rounding 

2181 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to 

2182 control the re-localization behavior. 

2183 

2184 Examples 

2185 -------- 

2186 **DatetimeIndex** 

2187 

2188 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min") 

2189 >>> rng 

2190 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00', 

2191 '2018-01-01 12:01:00'], 

2192 dtype='datetime64[us]', freq='min') 

2193 

2194 >>> rng.round('h') 

2195 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', 

2196 '2018-01-01 12:00:00'], 

2197 dtype='datetime64[us]', freq=None) 

2198 

2199 **Series** 

2200 

2201 >>> pd.Series(rng).dt.round("h") 

2202 0 2018-01-01 12:00:00 

2203 1 2018-01-01 12:00:00 

2204 2 2018-01-01 12:00:00 

2205 dtype: datetime64[us] 

2206 

2207 When rounding near a daylight savings time transition, use ``ambiguous`` or 

2208 ``nonexistent`` to control how the timestamp should be re-localized. 

2209 

2210 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 03:30:00"], tz="Europe/Amsterdam") 

2211 

2212 >>> rng_tz.floor("2h", ambiguous=False) 

2213 DatetimeIndex(['2021-10-31 02:00:00+01:00'], 

2214 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2215 

2216 >>> rng_tz.floor("2h", ambiguous=True) 

2217 DatetimeIndex(['2021-10-31 02:00:00+02:00'], 

2218 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2219 """ 

2220 return self._round(freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent) 

2221 

2222 def floor( 

2223 self, 

2224 freq, 

2225 ambiguous: TimeAmbiguous = "raise", 

2226 nonexistent: TimeNonexistent = "raise", 

2227 ) -> Self: 

2228 """ 

2229 Perform floor operation on the data to the specified `freq`. 

2230 

2231 Parameters 

2232 ---------- 

2233 freq : str or Offset 

2234 The frequency level to floor the index to. Must be a fixed 

2235 frequency like 's' (second) not 'ME' (month end). See 

2236 :ref:`frequency aliases <timeseries.offset_aliases>` for 

2237 a list of possible `freq` values. 

2238 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' 

2239 Only relevant for DatetimeIndex: 

2240 

2241 - 'infer' will attempt to infer fall dst-transition hours based on 

2242 order 

2243 - bool-ndarray where True signifies a DST time, False designates 

2244 a non-DST time (note that this flag is only applicable for 

2245 ambiguous times) 

2246 - 'NaT' will return NaT where there are ambiguous times 

2247 - 'raise' will raise a ValueError if there are ambiguous 

2248 times. 

2249 

2250 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \ 

2251 default 'raise' 

2252 A nonexistent time does not exist in a particular timezone 

2253 where clocks moved forward due to DST. 

2254 

2255 - 'shift_forward' will shift the nonexistent time forward to the 

2256 closest existing time 

2257 - 'shift_backward' will shift the nonexistent time backward to the 

2258 closest existing time 

2259 - 'NaT' will return NaT where there are nonexistent times 

2260 - timedelta objects will shift nonexistent times by the timedelta 

2261 - 'raise' will raise a ValueError if there are 

2262 nonexistent times. 

2263 

2264 Returns 

2265 ------- 

2266 DatetimeIndex, TimedeltaIndex, or Series 

2267 Index of the same type for a DatetimeIndex or TimedeltaIndex, 

2268 or a Series with the same index for a Series. 

2269 

2270 Raises 

2271 ------ 

2272 ValueError if the `freq` cannot be converted. 

2273 

2274 See Also 

2275 -------- 

2276 DatetimeIndex.floor : 

2277 Perform floor operation on the data to the specified `freq`. 

2278 DatetimeIndex.snap : 

2279 Snap time stamps to nearest occurring frequency. 

2280 

2281 Notes 

2282 ----- 

2283 If the timestamps have a timezone, flooring will take place relative to the 

2284 local ("wall") time and re-localized to the same timezone. When flooring 

2285 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to 

2286 control the re-localization behavior. 

2287 

2288 Examples 

2289 -------- 

2290 **DatetimeIndex** 

2291 

2292 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min") 

2293 >>> rng 

2294 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00', 

2295 '2018-01-01 12:01:00'], 

2296 dtype='datetime64[us]', freq='min') 

2297 

2298 >>> rng.floor('h') 

2299 DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00', 

2300 '2018-01-01 12:00:00'], 

2301 dtype='datetime64[us]', freq=None) 

2302 

2303 **Series** 

2304 

2305 >>> pd.Series(rng).dt.floor("h") 

2306 0 2018-01-01 11:00:00 

2307 1 2018-01-01 12:00:00 

2308 2 2018-01-01 12:00:00 

2309 dtype: datetime64[us] 

2310 

2311 When rounding near a daylight savings time transition, use ``ambiguous`` or 

2312 ``nonexistent`` to control how the timestamp should be re-localized. 

2313 

2314 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 03:30:00"], tz="Europe/Amsterdam") 

2315 

2316 >>> rng_tz.floor("2h", ambiguous=False) 

2317 DatetimeIndex(['2021-10-31 02:00:00+01:00'], 

2318 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2319 

2320 >>> rng_tz.floor("2h", ambiguous=True) 

2321 DatetimeIndex(['2021-10-31 02:00:00+02:00'], 

2322 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2323 """ 

2324 return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent) 

2325 

2326 def ceil( 

2327 self, 

2328 freq, 

2329 ambiguous: TimeAmbiguous = "raise", 

2330 nonexistent: TimeNonexistent = "raise", 

2331 ) -> Self: 

2332 """ 

2333 Perform ceil operation on the data to the specified `freq`. 

2334 

2335 Parameters 

2336 ---------- 

2337 freq : str or Offset 

2338 The frequency level to ceil the index to. Must be a fixed 

2339 frequency like 's' (second) not 'ME' (month end). See 

2340 :ref:`frequency aliases <timeseries.offset_aliases>` for 

2341 a list of possible `freq` values. 

2342 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise' 

2343 Only relevant for DatetimeIndex: 

2344 

2345 - 'infer' will attempt to infer fall dst-transition hours based on 

2346 order 

2347 - bool-ndarray where True signifies a DST time, False designates 

2348 a non-DST time (note that this flag is only applicable for 

2349 ambiguous times) 

2350 - 'NaT' will return NaT where there are ambiguous times 

2351 - 'raise' will raise a ValueError if there are ambiguous 

2352 times. 

2353 

2354 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \ 

2355 default 'raise' 

2356 A nonexistent time does not exist in a particular timezone 

2357 where clocks moved forward due to DST. 

2358 

2359 - 'shift_forward' will shift the nonexistent time forward to the 

2360 closest existing time 

2361 - 'shift_backward' will shift the nonexistent time backward to the 

2362 closest existing time 

2363 - 'NaT' will return NaT where there are nonexistent times 

2364 - timedelta objects will shift nonexistent times by the timedelta 

2365 - 'raise' will raise a ValueError if there are 

2366 nonexistent times. 

2367 

2368 Returns 

2369 ------- 

2370 DatetimeIndex, TimedeltaIndex, or Series 

2371 Index of the same type for a DatetimeIndex or TimedeltaIndex, 

2372 or a Series with the same index for a Series. 

2373 

2374 Raises 

2375 ------ 

2376 ValueError if the `freq` cannot be converted. 

2377 

2378 See Also 

2379 -------- 

2380 DatetimeIndex.floor : 

2381 Perform floor operation on the data to the specified `freq`. 

2382 DatetimeIndex.snap : 

2383 Snap time stamps to nearest occurring frequency. 

2384 

2385 Notes 

2386 ----- 

2387 If the timestamps have a timezone, ceiling will take place relative to the 

2388 local ("wall") time and re-localized to the same timezone. When ceiling 

2389 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to 

2390 control the re-localization behavior. 

2391 

2392 Examples 

2393 -------- 

2394 **DatetimeIndex** 

2395 

2396 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min") 

2397 >>> rng 

2398 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00', 

2399 '2018-01-01 12:01:00'], 

2400 dtype='datetime64[us]', freq='min') 

2401 

2402 >>> rng.ceil('h') 

2403 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00', 

2404 '2018-01-01 13:00:00'], 

2405 dtype='datetime64[us]', freq=None) 

2406 

2407 **Series** 

2408 

2409 >>> pd.Series(rng).dt.ceil("h") 

2410 0 2018-01-01 12:00:00 

2411 1 2018-01-01 12:00:00 

2412 2 2018-01-01 13:00:00 

2413 dtype: datetime64[us] 

2414 

2415 When rounding near a daylight savings time transition, use ``ambiguous`` or 

2416 ``nonexistent`` to control how the timestamp should be re-localized. 

2417 

2418 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 01:30:00"], tz="Europe/Amsterdam") 

2419 

2420 >>> rng_tz.ceil("h", ambiguous=False) 

2421 DatetimeIndex(['2021-10-31 02:00:00+01:00'], 

2422 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2423 

2424 >>> rng_tz.ceil("h", ambiguous=True) 

2425 DatetimeIndex(['2021-10-31 02:00:00+02:00'], 

2426 dtype='datetime64[us, Europe/Amsterdam]', freq=None) 

2427 """ 

2428 return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent) 

2429 

2430 # -------------------------------------------------------------- 

2431 # Reductions 

2432 

2433 def any(self, *, axis: AxisInt | None = None, skipna: bool = True) -> bool: 

2434 # GH#34479 the nanops call will raise a TypeError for non-td64 dtype 

2435 return nanops.nanany(self._ndarray, axis=axis, skipna=skipna, mask=self.isna()) 

2436 

2437 def all(self, *, axis: AxisInt | None = None, skipna: bool = True) -> bool: 

2438 # GH#34479 the nanops call will raise a TypeError for non-td64 dtype 

2439 

2440 return nanops.nanall(self._ndarray, axis=axis, skipna=skipna, mask=self.isna()) 

2441 

2442 # -------------------------------------------------------------- 

2443 # Frequency Methods 

2444 

2445 def _maybe_clear_freq(self) -> None: 

2446 self._freq = None 

2447 

2448 def _with_freq(self, freq) -> Self: 

2449 """ 

2450 Helper to get a view on the same data, with a new freq. 

2451 

2452 Parameters 

2453 ---------- 

2454 freq : DateOffset, None, or "infer" 

2455 

2456 Returns 

2457 ------- 

2458 Same type as self 

2459 """ 

2460 # GH#29843 

2461 if freq is None: 

2462 # Always valid 

2463 pass 

2464 elif len(self) == 0 and isinstance(freq, BaseOffset): 

2465 # Always valid. In the TimedeltaArray case, we require a Tick offset 

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

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

2468 else: 

2469 # As an internal method, we can ensure this assertion always holds 

2470 assert freq == "infer" 

2471 freq = to_offset(self.inferred_freq) 

2472 

2473 arr = self.view() 

2474 arr._freq = freq 

2475 return arr 

2476 

2477 # -------------------------------------------------------------- 

2478 # ExtensionArray Interface 

2479 

2480 def _values_for_json(self) -> np.ndarray: 

2481 # Small performance bump vs the base class which calls np.asarray(self) 

2482 if isinstance(self.dtype, np.dtype): 

2483 return self._ndarray 

2484 return super()._values_for_json() 

2485 

2486 def factorize( 

2487 self, 

2488 use_na_sentinel: bool = True, 

2489 sort: bool = False, 

2490 ): 

2491 if self.freq is not None: 

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

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

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

2495 uniques = self[::-1] 

2496 else: 

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

2498 uniques = self.copy() # TODO: copy or view? 

2499 return codes, uniques 

2500 

2501 if sort: 

2502 # algorithms.factorize only passes sort=True here when freq is 

2503 # not None, so this should not be reached. 

2504 raise NotImplementedError( 

2505 f"The 'sort' keyword in {type(self).__name__}.factorize is " 

2506 "ignored unless arr.freq is not None. To factorize with sort, " 

2507 "call pd.factorize(obj, sort=True) instead." 

2508 ) 

2509 return super().factorize(use_na_sentinel=use_na_sentinel) 

2510 

2511 @classmethod 

2512 def _concat_same_type( 

2513 cls, 

2514 to_concat: Sequence[Self], 

2515 axis: AxisInt = 0, 

2516 ) -> Self: 

2517 new_obj = super()._concat_same_type(to_concat, axis) 

2518 

2519 obj = to_concat[0] 

2520 

2521 if axis == 0: 

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

2523 # original frequency 

2524 to_concat = [x for x in to_concat if len(x)] 

2525 

2526 if obj.freq is not None and all(x.freq == obj.freq for x in to_concat): 

2527 pairs = zip(to_concat[:-1], to_concat[1:], strict=True) 

2528 if all(pair[0][-1] + obj.freq == pair[1][0] for pair in pairs): 

2529 new_freq = obj.freq 

2530 new_obj._freq = new_freq 

2531 return new_obj 

2532 

2533 def copy(self, order: str = "C") -> Self: 

2534 new_obj = super().copy(order=order) 

2535 new_obj._freq = self.freq 

2536 return new_obj 

2537 

2538 def interpolate( 

2539 self, 

2540 *, 

2541 method: InterpolateOptions, 

2542 axis: int, 

2543 index: Index, 

2544 limit, 

2545 limit_direction, 

2546 limit_area, 

2547 copy: bool, 

2548 **kwargs, 

2549 ) -> Self: 

2550 """ 

2551 See NDFrame.interpolate.__doc__. 

2552 """ 

2553 # NB: we return type(self) even if copy=False 

2554 if method != "linear": 

2555 raise NotImplementedError 

2556 

2557 if not copy: 

2558 out_data = self._ndarray 

2559 else: 

2560 out_data = self._ndarray.copy() 

2561 

2562 missing.interpolate_2d_inplace( 

2563 out_data, 

2564 method=method, 

2565 axis=axis, 

2566 index=index, 

2567 limit=limit, 

2568 limit_direction=limit_direction, 

2569 limit_area=limit_area, 

2570 **kwargs, 

2571 ) 

2572 if not copy: 

2573 return self 

2574 return type(self)._simple_new(out_data, dtype=self.dtype) 

2575 

2576 def take( 

2577 self, 

2578 indices: TakeIndexer, 

2579 *, 

2580 allow_fill: bool = False, 

2581 fill_value: Any = None, 

2582 axis: AxisInt = 0, 

2583 ) -> Self: 

2584 result = super().take( 

2585 indices=indices, allow_fill=allow_fill, fill_value=fill_value, axis=axis 

2586 ) 

2587 

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

2589 maybe_slice = lib.maybe_indices_to_slice(indices, len(self)) # type: ignore[arg-type] 

2590 

2591 if isinstance(maybe_slice, slice): 

2592 freq = self._get_getitem_freq(maybe_slice) 

2593 result._freq = freq # type: ignore[assignment] 

2594 

2595 return result 

2596 

2597 # -------------------------------------------------------------- 

2598 # Unsorted 

2599 

2600 @property 

2601 def _is_dates_only(self) -> bool: 

2602 """ 

2603 Check if we are round times at midnight (and no timezone), which will 

2604 be given a more compact __repr__ than other cases. For TimedeltaArray 

2605 we are checking for multiples of 24H. 

2606 """ 

2607 if not lib.is_np_dtype(self.dtype): 

2608 # i.e. we have a timezone 

2609 return False 

2610 

2611 values_int = self.asi8 

2612 consider_values = values_int != iNaT 

2613 reso = get_unit_from_dtype(self.dtype) 

2614 ppd = periods_per_day(reso) 

2615 

2616 # TODO: can we reuse is_date_array_normalized? would need a skipna kwd 

2617 # (first attempt at this was less performant than this implementation) 

2618 even_days = np.logical_and(consider_values, values_int % ppd != 0).sum() == 0 

2619 return even_days 

2620 

2621 

2622# ------------------------------------------------------------------- 

2623# Shared Constructor Helpers 

2624 

2625 

2626def ensure_arraylike_for_datetimelike( 

2627 data, copy: bool, cls_name: str 

2628) -> tuple[ArrayLike, bool]: 

2629 if not hasattr(data, "dtype"): 

2630 # e.g. list, tuple 

2631 if not isinstance(data, (list, tuple)) and np.ndim(data) == 0: 

2632 # i.e. generator 

2633 data = list(data) 

2634 

2635 data = construct_1d_object_array_from_listlike(data) 

2636 copy = False 

2637 elif isinstance(data, ABCMultiIndex): 

2638 raise TypeError(f"Cannot create a {cls_name} from a MultiIndex.") 

2639 else: 

2640 data = extract_array(data, extract_numpy=True) 

2641 

2642 if isinstance(data, IntegerArray) or ( 

2643 isinstance(data, ArrowExtensionArray) and data.dtype.kind in "iu" 

2644 ): 

2645 data = data.to_numpy("int64", na_value=iNaT) 

2646 copy = False 

2647 elif isinstance(data, ArrowExtensionArray): 

2648 data = data._maybe_convert_datelike_array() 

2649 data = data.to_numpy() 

2650 copy = False 

2651 elif not isinstance(data, (np.ndarray, ExtensionArray)): 

2652 # GH#24539 e.g. xarray, dask object 

2653 data = np.asarray(data) 

2654 

2655 elif isinstance(data, ABCCategorical): 

2656 # GH#18664 preserve tz in going DTI->Categorical->DTI 

2657 # TODO: cases where we need to do another pass through maybe_convert_dtype, 

2658 # e.g. the categories are timedelta64s 

2659 data = data.categories.take(data.codes, fill_value=NaT)._values 

2660 copy = False 

2661 

2662 return data, copy 

2663 

2664 

2665@overload 

2666def validate_periods(periods: None) -> None: ... 

2667 

2668 

2669@overload 

2670def validate_periods(periods: int) -> int: ... 

2671 

2672 

2673def validate_periods(periods: int | None) -> int | None: 

2674 """ 

2675 If a `periods` argument is passed to the Datetime/Timedelta Array/Index 

2676 constructor, cast it to an integer. 

2677 

2678 Parameters 

2679 ---------- 

2680 periods : None, int 

2681 

2682 Returns 

2683 ------- 

2684 periods : None or int 

2685 

2686 Raises 

2687 ------ 

2688 TypeError 

2689 if periods is not None or int 

2690 """ 

2691 if periods is not None and not lib.is_integer(periods): 

2692 raise TypeError(f"periods must be an integer, got {periods}") 

2693 # error: Incompatible return value type (got "int | integer[Any] | None", 

2694 # expected "int | None") 

2695 return periods # type: ignore[return-value] 

2696 

2697 

2698def _validate_inferred_freq( 

2699 freq: BaseOffset | None, inferred_freq: BaseOffset | None 

2700) -> BaseOffset | None: 

2701 """ 

2702 If the user passes a freq and another freq is inferred from passed data, 

2703 require that they match. 

2704 

2705 Parameters 

2706 ---------- 

2707 freq : DateOffset or None 

2708 inferred_freq : DateOffset or None 

2709 

2710 Returns 

2711 ------- 

2712 freq : DateOffset or None 

2713 """ 

2714 if inferred_freq is not None: 

2715 if freq is not None and freq != inferred_freq: 

2716 raise ValueError( 

2717 f"Inferred frequency {inferred_freq} from passed " 

2718 "values does not conform to passed frequency " 

2719 f"{freq.freqstr}" 

2720 ) 

2721 if freq is None: 

2722 freq = inferred_freq 

2723 

2724 return freq 

2725 

2726 

2727def dtype_to_unit(dtype: DatetimeTZDtype | np.dtype | ArrowDtype) -> str: 

2728 """ 

2729 Return the unit str corresponding to the dtype's resolution. 

2730 

2731 Parameters 

2732 ---------- 

2733 dtype : DatetimeTZDtype or np.dtype 

2734 If np.dtype, we assume it is a datetime64 dtype. 

2735 

2736 Returns 

2737 ------- 

2738 str 

2739 """ 

2740 if isinstance(dtype, DatetimeTZDtype): 

2741 return dtype.unit 

2742 elif isinstance(dtype, ArrowDtype): 

2743 if dtype.kind not in "mM": 

2744 raise ValueError(f"{dtype=} does not have a resolution.") 

2745 return dtype.pyarrow_dtype.unit 

2746 return np.datetime_data(dtype)[0]