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 cast,
16 final,
17 overload,
18)
19import warnings
20
21import numpy as np
22
23from pandas._config import using_string_dtype
24from pandas._config.config import _global_config as config
25
26from pandas._libs import (
27 algos,
28 groupby as libgroupby,
29 lib,
30)
31from pandas._libs.tslibs import (
32 BaseOffset,
33 Day,
34 IncompatibleFrequency,
35 NaT,
36 NaTType,
37 Period,
38 Tick,
39 Timedelta,
40 Timestamp,
41 add_overflowsafe,
42 astype_overflowsafe,
43 get_unit_from_dtype,
44 iNaT,
45 ints_to_pydatetime,
46 ints_to_pytimedelta,
47 periods_per_day,
48)
49from pandas._libs.tslibs.fields import (
50 RoundTo,
51 round_nsint64,
52)
53from pandas._libs.tslibs.np_datetime import compare_mismatched_resolutions
54from pandas._libs.tslibs.timedeltas import get_unit_for_round
55from pandas._libs.tslibs.timestamps import integer_op_not_supported
56from pandas._typing import (
57 ArrayLike,
58 AxisInt,
59 DatetimeLikeScalar,
60 Dtype,
61 DtypeObj,
62 F,
63 InterpolateOptions,
64 NpDtype,
65 PositionalIndexer2D,
66 PositionalIndexerTuple,
67 ScalarIndexer,
68 SequenceIndexer,
69 TimeAmbiguous,
70 TimeNonexistent,
71 npt,
72)
73from pandas.compat.numpy import function as nv
74from pandas.errors import (
75 AbstractMethodError,
76 InvalidComparison,
77 Pandas4Warning,
78 PerformanceWarning,
79)
80from pandas.util._decorators import (
81 cache_readonly,
82)
83from pandas.util._exceptions import find_stack_level
84
85from pandas.core.dtypes.cast import construct_1d_object_array_from_listlike
86from pandas.core.dtypes.common import (
87 is_all_strings,
88 is_integer_dtype,
89 is_list_like,
90 is_object_dtype,
91 is_string_dtype,
92 pandas_dtype,
93)
94from pandas.core.dtypes.dtypes import (
95 ArrowDtype,
96 CategoricalDtype,
97 DatetimeTZDtype,
98 ExtensionDtype,
99 PeriodDtype,
100)
101from pandas.core.dtypes.generic import (
102 ABCCategorical,
103 ABCMultiIndex,
104)
105from pandas.core.dtypes.missing import (
106 is_valid_na_for_dtype,
107 isna,
108)
109
110from pandas.core import (
111 algorithms,
112 missing,
113 nanops,
114 ops,
115)
116from pandas.core.algorithms import (
117 isin,
118 map_array,
119 unique1d,
120)
121from pandas.core.array_algos import datetimelike_accumulations
122from pandas.core.arraylike import OpsMixin
123from pandas.core.arrays._mixins import (
124 NDArrayBackedExtensionArray,
125 ravel_compat,
126)
127from pandas.core.arrays.arrow.array import ArrowExtensionArray
128from pandas.core.arrays.base import ExtensionArray
129from pandas.core.arrays.integer import IntegerArray
130from pandas.core.construction import (
131 array as pd_array,
132 ensure_wrapped_if_datetimelike,
133 extract_array,
134)
135from pandas.core.indexers import check_setitem_lengths
136from pandas.core.ops.common import unpack_zerodim_and_defer
137from pandas.core.ops.invalid import (
138 invalid_comparison,
139 make_invalid_op,
140)
141
142from pandas.tseries import frequencies
143
144if TYPE_CHECKING:
145 from collections.abc import (
146 Callable,
147 Iterator,
148 Sequence,
149 )
150
151 from pandas._typing import TimeUnit
152
153 from pandas import Index
154 from pandas.core.arrays import (
155 DatetimeArray,
156 PeriodArray,
157 TimedeltaArray,
158 )
159
160DTScalarOrNaT: TypeAlias = DatetimeLikeScalar | NaTType
161
162
163def _make_unpacked_invalid_op(op_name: str):
164 op = make_invalid_op(op_name)
165 return unpack_zerodim_and_defer(op_name)(op)
166
167
168def _period_dispatch(meth: F) -> F:
169 """
170 For PeriodArray methods, dispatch to DatetimeArray and re-wrap the results
171 in PeriodArray. We cannot use ._ndarray directly for the affected
172 methods because the i8 data has different semantics on NaT values.
173 """
174
175 @wraps(meth)
176 def new_meth(self, *args, **kwargs):
177 if not isinstance(self.dtype, PeriodDtype):
178 return meth(self, *args, **kwargs)
179
180 arr = self.view("M8[ns]")
181 result = meth(arr, *args, **kwargs)
182 if result is NaT:
183 return NaT
184 elif isinstance(result, Timestamp):
185 return self._box_func(result._value)
186
187 res_i8 = result.view("i8")
188 return self._from_backing_data(res_i8)
189
190 return cast("F", new_meth)
191
192
193class DatetimeLikeArrayMixin(OpsMixin, NDArrayBackedExtensionArray):
194 """
195 Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray
196
197 Assumes that __new__/__init__ defines:
198 _ndarray
199 """
200
201 _is_recognized_dtype: Callable[[DtypeObj], bool]
202 _recognized_scalars: tuple[type, ...]
203 _ndarray: np.ndarray
204
205 @cache_readonly
206 def _can_hold_na(self) -> bool:
207 return True
208
209 def __init__(
210 self, data, dtype: Dtype | None = None, freq=None, copy: bool = False
211 ) -> None:
212 raise AbstractMethodError(self)
213
214 @property
215 def _scalar_type(self) -> type[DatetimeLikeScalar]:
216 """
217 The scalar associated with this datelike
218
219 * PeriodArray : Period
220 * DatetimeArray : Timestamp
221 * TimedeltaArray : Timedelta
222 """
223 raise AbstractMethodError(self)
224
225 def _scalar_from_string(self, value: str) -> DTScalarOrNaT:
226 """
227 Construct a scalar type from a string.
228
229 Parameters
230 ----------
231 value : str
232
233 Returns
234 -------
235 Period, Timestamp, or Timedelta, or NaT
236 Whatever the type of ``self._scalar_type`` is.
237
238 Notes
239 -----
240 This should call ``self._check_compatible_with`` before
241 unboxing the result.
242 """
243 raise AbstractMethodError(self)
244
245 def _unbox_scalar(
246 self, value: DTScalarOrNaT
247 ) -> np.int64 | np.datetime64 | np.timedelta64:
248 """
249 Unbox the integer value of a scalar `value`.
250
251 Parameters
252 ----------
253 value : Period, Timestamp, Timedelta, or NaT
254 Depending on subclass.
255
256 Returns
257 -------
258 int
259
260 Examples
261 --------
262 >>> arr = pd.array(np.array(["1970-01-01"], "datetime64[ns]"))
263 >>> arr._unbox_scalar(arr[0])
264 np.datetime64('1970-01-01T00:00:00.000000000')
265 """
266 raise AbstractMethodError(self)
267
268 def _check_compatible_with(self, other: DTScalarOrNaT) -> None:
269 """
270 Verify that `self` and `other` are compatible.
271
272 * DatetimeArray verifies that the timezones (if any) match
273 * PeriodArray verifies that the freq matches
274 * Timedelta has no verification
275
276 In each case, NaT is considered compatible.
277
278 Parameters
279 ----------
280 other
281
282 Raises
283 ------
284 Exception
285 """
286 raise AbstractMethodError(self)
287
288 # ------------------------------------------------------------------
289
290 def _box_func(self, x):
291 """
292 box function to get object from internal representation
293 """
294 raise AbstractMethodError(self)
295
296 def _box_values(self, values) -> np.ndarray:
297 """
298 apply box func to passed values
299 """
300 return lib.map_infer(values, self._box_func, convert=False)
301
302 def __iter__(self) -> Iterator[Any]:
303 if self.ndim > 1:
304 return (self[n] for n in range(len(self)))
305 else:
306 return (self._box_func(v) for v in self.asi8)
307
308 @property
309 def asi8(self) -> npt.NDArray[np.int64]:
310 """
311 Integer representation of the values.
312
313 Returns
314 -------
315 ndarray
316 An ndarray with int64 dtype.
317 """
318 # do not cache or you'll create a memory leak
319 return self._ndarray.view("i8")
320
321 # ----------------------------------------------------------------
322 # Rendering Methods
323
324 def _format_native_types(
325 self, *, na_rep: str | float = "NaT", date_format=None
326 ) -> npt.NDArray[np.object_]:
327 """
328 Helper method for astype when converting to strings.
329
330 Returns
331 -------
332 ndarray[str]
333 """
334 raise AbstractMethodError(self)
335
336 def _formatter(self, boxed: bool = False) -> Callable[[object], str]:
337 # TODO: Remove Datetime & DatetimeTZ formatters.
338 return "'{}'".format
339
340 # ----------------------------------------------------------------
341 # Array-Like / EA-Interface Methods
342
343 def __array__(
344 self, dtype: NpDtype | None = None, copy: bool | None = None
345 ) -> np.ndarray:
346 # used for Timedelta/DatetimeArray, overwritten by PeriodArray
347 if is_object_dtype(dtype):
348 if copy is False:
349 raise ValueError(
350 "Unable to avoid copy while creating an array as requested."
351 )
352 return np.array(list(self), dtype=object)
353
354 if copy is True:
355 return np.array(self._ndarray, dtype=dtype)
356
357 result = self._ndarray
358 if self._readonly:
359 result = result.view()
360 result.flags.writeable = False
361 return result
362
363 @overload
364 def __getitem__(self, key: ScalarIndexer) -> DTScalarOrNaT: ...
365
366 @overload
367 def __getitem__(
368 self,
369 key: SequenceIndexer | PositionalIndexerTuple,
370 ) -> Self: ...
371
372 def __getitem__(self, key: PositionalIndexer2D) -> Self | DTScalarOrNaT:
373 """
374 This getitem defers to the underlying array, which by-definition can
375 only handle list-likes, slices, and integer scalars
376 """
377 # Use cast as we know we will get back a DatetimeLikeArray or DTScalar,
378 # but skip evaluating the Union at runtime for performance
379 # (see https://github.com/pandas-dev/pandas/pull/44624)
380 result = cast("Self | DTScalarOrNaT", super().__getitem__(key))
381 if lib.is_scalar(result):
382 return result
383 else:
384 # At this point we know the result is an array.
385 result = cast("Self", result)
386 return result
387
388 # error: Argument 1 of "__setitem__" is incompatible with supertype
389 # "ExtensionArray"; supertype defines the argument type as "Union[int,
390 # ndarray]"
391 def __setitem__(
392 self,
393 key: int | Sequence[int] | Sequence[bool] | slice,
394 value: NaTType | Any | Sequence[Any],
395 ) -> None:
396 # I'm fudging the types a bit here. "Any" above really depends
397 # on type(self). For PeriodArray, it's Period (or stuff coercible
398 # to a period in from_sequence). For DatetimeArray, it's Timestamp...
399 # I don't know if mypy can do that, possibly with Generics.
400 # https://mypy.readthedocs.io/en/latest/generics.html
401
402 check_setitem_lengths(key, value, self)
403
404 # Calling super() before the no_op short-circuit means that we raise
405 # on invalid 'value' even if this is a no-op, e.g. wrong-dtype empty array.
406 super().__setitem__(key, value)
407
408 def astype(self, dtype, copy: bool = True):
409 # Some notes on cases we don't have to handle here in the base class:
410 # 1. PeriodArray.astype handles period -> period
411 # 2. DatetimeArray.astype handles conversion between tz.
412 # 3. DatetimeArray.astype handles datetime -> period
413 dtype = pandas_dtype(dtype)
414
415 if dtype == object:
416 if self.dtype.kind == "M":
417 self = cast("DatetimeArray", self)
418 # *much* faster than self._box_values
419 # for e.g. test_get_loc_tuple_monotonic_above_size_cutoff
420 i8data = self.asi8
421 converted = ints_to_pydatetime(
422 i8data,
423 tz=self.tz,
424 box="timestamp",
425 reso=self._creso,
426 )
427 return converted
428
429 elif self.dtype.kind == "m":
430 return ints_to_pytimedelta(self._ndarray, box=True)
431
432 return self._box_values(self.asi8.ravel()).reshape(self.shape)
433
434 elif is_string_dtype(dtype):
435 if isinstance(dtype, ExtensionDtype):
436 arr_object = self._format_native_types(na_rep=dtype.na_value) # type: ignore[arg-type]
437 cls = dtype.construct_array_type()
438 return cls._from_sequence(arr_object, dtype=dtype, copy=False)
439 else:
440 return self._format_native_types()
441
442 elif isinstance(dtype, ExtensionDtype):
443 return super().astype(dtype, copy=copy)
444 elif dtype.kind in "iu":
445 # we deliberately ignore int32 vs. int64 here.
446 # See https://github.com/pandas-dev/pandas/issues/24381 for more.
447 values = self.asi8
448 if dtype != np.int64:
449 raise TypeError(
450 f"Converting from {self.dtype} to {dtype} is not supported. "
451 "Do obj.astype('int64').astype(dtype) instead"
452 )
453
454 if copy:
455 values = values.copy()
456 return values
457 elif (dtype.kind in "mM" and self.dtype != dtype) or dtype.kind == "f":
458 # disallow conversion between datetime/timedelta,
459 # and conversions for any datetimelike to float
460 msg = f"Cannot cast {type(self).__name__} to dtype {dtype}"
461 raise TypeError(msg)
462 else:
463 return np.asarray(self, dtype=dtype)
464
465 @overload # type: ignore[override]
466 def view(self) -> Self: ...
467
468 @overload
469 def view(self, dtype: Literal["M8[ns]"]) -> DatetimeArray: ...
470
471 @overload
472 def view(self, dtype: Literal["m8[ns]"]) -> TimedeltaArray: ...
473
474 @overload
475 def view(self, dtype: Dtype | None = ...) -> ArrayLike: ...
476
477 def view(self, dtype: Dtype | None = None) -> ArrayLike:
478 # we need to explicitly call super() method as long as the `@overload`s
479 # are present in this file.
480 return super().view(dtype)
481
482 # ------------------------------------------------------------------
483 # Validation Methods
484 # TODO: try to de-duplicate these, ensure identical behavior
485
486 def _validate_comparison_value(self, other):
487 if isinstance(other, str):
488 try:
489 # GH#18435 strings get a pass from tzawareness compat
490 other = self._scalar_from_string(other)
491 except (ValueError, IncompatibleFrequency) as err:
492 # failed to parse as Timestamp/Timedelta/Period
493 raise InvalidComparison(other) from err
494
495 if isinstance(other, self._recognized_scalars) or other is NaT:
496 # error: Argument 1 to "Timestamp" has incompatible type "object";
497 # expected "integer[Any] | float | str | date | datetime |
498 # datetime64[date | int | None]" [arg-type]
499 other = self._scalar_type(other) # type: ignore[arg-type]
500 try:
501 self._check_compatible_with(other)
502 except TypeError as err:
503 # e.g. tzawareness mismatch
504 raise InvalidComparison(other) from err
505
506 elif not is_list_like(other):
507 raise InvalidComparison(other)
508
509 elif len(other) != len(self):
510 raise ValueError("Lengths must match")
511
512 else:
513 try:
514 other = self._validate_listlike(other, allow_object=True)
515 self._check_compatible_with(other)
516 except TypeError as err:
517 if is_object_dtype(getattr(other, "dtype", None)):
518 # We will have to operate element-wise
519 pass
520 else:
521 raise InvalidComparison(other) from err
522
523 return other
524
525 def _validate_scalar(
526 self,
527 value,
528 *,
529 allow_listlike: bool = False,
530 unbox: bool = True,
531 ):
532 """
533 Validate that the input value can be cast to our scalar_type.
534
535 Parameters
536 ----------
537 value : object
538 allow_listlike: bool, default False
539 When raising an exception, whether the message should say
540 listlike inputs are allowed.
541 unbox : bool, default True
542 Whether to unbox the result before returning. Note: unbox=False
543 skips the setitem compatibility check.
544
545 Returns
546 -------
547 self._scalar_type or NaT
548 """
549 if isinstance(value, self._scalar_type):
550 pass
551
552 elif isinstance(value, str):
553 # NB: Careful about tzawareness
554 try:
555 value = self._scalar_from_string(value)
556 except ValueError as err:
557 msg = self._validation_error_message(value, allow_listlike)
558 raise TypeError(msg) from err
559
560 elif is_valid_na_for_dtype(value, self.dtype):
561 # GH#18295
562 value = NaT
563
564 elif isna(value):
565 # if we are dt64tz and value is dt64("NaT"), dont cast to NaT,
566 # or else we'll fail to raise in _unbox_scalar
567 msg = self._validation_error_message(value, allow_listlike)
568 raise TypeError(msg)
569
570 elif isinstance(value, self._recognized_scalars):
571 # error: Argument 1 to "Timestamp" has incompatible type "object"; expected
572 # "integer[Any] | float | str | date | datetime | datetime64"
573 value = self._scalar_type(value) # type: ignore[arg-type]
574
575 else:
576 msg = self._validation_error_message(value, allow_listlike)
577 raise TypeError(msg)
578
579 if not unbox:
580 # NB: In general NDArrayBackedExtensionArray will unbox here;
581 # this option exists to prevent a performance hit in
582 # TimedeltaIndex.get_loc
583 return value
584 return self._unbox_scalar(value)
585
586 def _validation_error_message(self, value, allow_listlike: bool = False) -> str:
587 """
588 Construct an exception message on validation error.
589
590 Some methods allow only scalar inputs, while others allow either scalar
591 or listlike.
592
593 Parameters
594 ----------
595 allow_listlike: bool, default False
596
597 Returns
598 -------
599 str
600 """
601 if hasattr(value, "dtype") and getattr(value, "ndim", 0) > 0:
602 msg_got = f"{value.dtype} array"
603 else:
604 msg_got = f"'{type(value).__name__}'"
605 if allow_listlike:
606 msg = (
607 f"value should be a '{self._scalar_type.__name__}', 'NaT', "
608 f"or array of those. Got {msg_got} instead."
609 )
610 else:
611 msg = (
612 f"value should be a '{self._scalar_type.__name__}' or 'NaT'. "
613 f"Got {msg_got} instead."
614 )
615 return msg
616
617 def _validate_listlike(self, value, allow_object: bool = False):
618 if isinstance(value, type(self)):
619 if self.dtype.kind in "mM" and not allow_object and self.unit != value.unit: # type: ignore[attr-defined]
620 # error: "DatetimeLikeArrayMixin" has no attribute "as_unit"
621 value = value.as_unit(self.unit, round_ok=False) # type: ignore[attr-defined]
622 return value
623
624 if isinstance(value, list) and len(value) == 0:
625 # We treat empty list as our own dtype.
626 return type(self)._from_sequence([], dtype=self.dtype)
627
628 if hasattr(value, "dtype") and value.dtype == object:
629 # `array` below won't do inference if value is an Index or Series.
630 # so do so here. in the Index case, inferred_type may be cached.
631 arr = np.asarray(value)
632 flat = arr.ravel() if arr.ndim != 1 else arr
633 converted = lib.maybe_convert_objects(
634 flat, convert_non_numeric=True, dtype_if_all_nat=self.dtype
635 )
636 converted = ensure_wrapped_if_datetimelike(converted)
637 if isinstance(converted, type(self)):
638 if (
639 self.dtype.kind in "mM"
640 and not allow_object
641 and self.unit != converted.unit # type: ignore[attr-defined]
642 ):
643 converted = converted.as_unit(self.unit, round_ok=False) # type: ignore[attr-defined]
644 if arr.ndim != 1:
645 converted = converted.reshape(arr.shape)
646 return converted
647 elif self.dtype.kind == "M" and lib.infer_dtype(value) == "date":
648 # GH#65056
649 warnings.warn(
650 "Inferring datetime64 from data containing datetime.date "
651 "objects is deprecated. In a future version, these will "
652 "be left as object dtype. Use "
653 "pd.to_datetime to explicitly convert "
654 "to datetime64.",
655 Pandas4Warning,
656 stacklevel=find_stack_level(),
657 )
658 value = type(self)._from_sequence(value)
659
660 if isinstance(value, list):
661 value = construct_1d_object_array_from_listlike(value)
662 if isinstance(value, np.ndarray) and value.dtype == object:
663 # We need to call maybe_convert_objects here instead of in pd_array
664 # so we can specify dtype_if_all_nat.
665 value = lib.maybe_convert_objects(
666 value, convert_non_numeric=True, dtype_if_all_nat=self.dtype
667 )
668 # Do type inference if necessary up front (after unpacking
669 # NumpyExtensionArray)
670 # e.g. we passed PeriodIndex.values and got an ndarray of Periods
671 value = extract_array(value, extract_numpy=True)
672 value = pd_array(value)
673 value = extract_array(value, extract_numpy=True)
674
675 if is_all_strings(value):
676 # We got a StringArray
677 try:
678 # TODO: Could use from_sequence_of_strings if implemented
679 # Note: passing dtype is necessary for PeriodArray tests
680 value = type(self)._from_sequence(value, dtype=self.dtype)
681 except ValueError:
682 pass
683
684 if isinstance(value.dtype, CategoricalDtype):
685 # e.g. we have a Categorical holding self.dtype
686 if value.categories.dtype == self.dtype:
687 # TODO: do we need equal dtype or just comparable?
688 value = value._internal_get_values()
689 value = extract_array(value, extract_numpy=True)
690
691 if allow_object and is_object_dtype(value.dtype):
692 pass
693
694 elif not type(self)._is_recognized_dtype(value.dtype):
695 msg = self._validation_error_message(value, True)
696 raise TypeError(msg)
697
698 if self.dtype.kind in "mM" and not allow_object:
699 # error: "DatetimeLikeArrayMixin" has no attribute "as_unit"
700 value = value.as_unit(self.unit, round_ok=False) # type: ignore[attr-defined]
701 return value
702
703 def _validate_setitem_value(self, value):
704 if is_list_like(value):
705 value = self._validate_listlike(value)
706 else:
707 return self._validate_scalar(value, allow_listlike=True)
708
709 return self._unbox(value)
710
711 @final
712 def _unbox(self, other) -> np.int64 | np.datetime64 | np.timedelta64 | np.ndarray:
713 """
714 Unbox either a scalar with _unbox_scalar or an instance of our own type.
715 """
716 if lib.is_scalar(other):
717 other = self._unbox_scalar(other)
718 else:
719 # same type as self
720 self._check_compatible_with(other)
721 other = other._ndarray
722 return other
723
724 def fillna(self, value, limit: int | None = None, copy: bool = True) -> Self:
725 # Fast path: single-pass Cython using iNaT sentinel. GH#42147
726 if lib.is_scalar(value):
727 if not self._hasna:
728 return self.copy() if copy else self[:]
729 try:
730 validated = self._validate_setitem_value(value)
731 except (ValueError, TypeError):
732 pass
733 else:
734 if copy:
735 new_ndarray = self._ndarray.copy()
736 else:
737 if self._readonly:
738 raise ValueError("Cannot modify read-only array")
739 new_ndarray = self._ndarray
740
741 arr_i8 = new_ndarray.view("i8")
742 fill_i8 = np.array(validated, dtype=new_ndarray.dtype).view("i8")[()]
743 algos.scalar_fillna_inplace(
744 arr_i8, fill_i8, is_datetimelike=True, limit=limit
745 )
746
747 return type(self)._simple_new(new_ndarray, dtype=self.dtype)
748
749 return super().fillna(value, limit=limit, copy=copy)
750
751 # ------------------------------------------------------------------
752 # Additional array methods
753 # These are not part of the EA API, but we implement them because
754 # pandas assumes they're there.
755
756 @ravel_compat
757 def map(self, mapper, na_action: Literal["ignore"] | None = None):
758 from pandas import Index
759
760 result = map_array(self, mapper, na_action=na_action)
761 result = Index(result, copy=False)
762
763 if isinstance(result, ABCMultiIndex):
764 return result.to_numpy()
765 else:
766 return result._data
767
768 def isin(self, values: ArrayLike) -> npt.NDArray[np.bool_]:
769 """
770 Compute boolean array of whether each value is found in the
771 passed set of values.
772
773 Parameters
774 ----------
775 values : np.ndarray or ExtensionArray
776
777 Returns
778 -------
779 ndarray[bool]
780 """
781 if values.dtype.kind in "fiuc":
782 # TODO: de-duplicate with equals, validate_comparison_value
783 return np.zeros(self.shape, dtype=bool)
784
785 values = ensure_wrapped_if_datetimelike(values)
786
787 if not isinstance(values, type(self)):
788 if values.dtype == object:
789 values = lib.maybe_convert_objects(
790 values, # type: ignore[arg-type]
791 convert_non_numeric=True,
792 dtype_if_all_nat=self.dtype,
793 )
794 if values.dtype != object:
795 return self.isin(values)
796 else:
797 # TODO: Deprecate this case
798 # https://github.com/pandas-dev/pandas/pull/58645/files#r1604055791
799 return isin(self.astype(object), values)
800 return np.zeros(self.shape, dtype=bool)
801
802 if self.dtype.kind in "mM":
803 self = cast("DatetimeArray | TimedeltaArray", self)
804 # Cast to the higher resolution to avoid silently truncating
805 # finer-resolution values, which could lead to false matches.
806 self, values = self._ensure_matching_resos(values)
807
808 try:
809 # error: Argument 1 to "_check_compatible_with" of "DatetimeLikeArrayMixin"
810 # has incompatible type "ExtensionArray | ndarray[Any, Any]"; expected
811 # "Period | Timestamp | Timedelta | NaTType"
812 self._check_compatible_with(values) # type: ignore[arg-type]
813 except (TypeError, ValueError):
814 # Includes tzawareness mismatch and IncompatibleFrequencyError
815 return np.zeros(self.shape, dtype=bool)
816
817 # error: Item "ExtensionArray" of "ExtensionArray | ndarray[Any, Any]"
818 # has no attribute "asi8"
819 return isin(self.asi8, values.asi8) # type: ignore[union-attr]
820
821 # ------------------------------------------------------------------
822 # Null Handling
823
824 def isna(self) -> npt.NDArray[np.bool_]:
825 return self._isnan
826
827 @property # NB: override with cache_readonly in immutable subclasses
828 def _isnan(self) -> npt.NDArray[np.bool_]:
829 """
830 return if each value is nan
831 """
832 return self.asi8 == iNaT
833
834 @property # NB: override with cache_readonly in immutable subclasses
835 def _hasna(self) -> bool:
836 """
837 return if I have any nans; enables various perf speedups
838 """
839 return bool(self._isnan.any())
840
841 def _maybe_mask_results(
842 self, result: np.ndarray, fill_value=iNaT, convert=None
843 ) -> np.ndarray:
844 """
845 Parameters
846 ----------
847 result : np.ndarray
848 fill_value : object, default iNaT
849 convert : str, dtype or None
850
851 Returns
852 -------
853 result : ndarray with values replace by the fill_value
854
855 mask the result if needed, convert to the provided dtype if its not
856 None
857
858 This is an internal routine.
859 """
860 if self._hasna:
861 if convert:
862 result = result.astype(convert)
863 if fill_value is None:
864 fill_value = np.nan
865 np.putmask(result, self._isnan, fill_value)
866 return result
867
868 # ------------------------------------------------------------------
869 # Frequency Properties/Methods
870
871 @property # NB: override with cache_readonly in immutable subclasses
872 def _inferred_freq_str(self) -> str | None:
873 """
874 Internal version of inferred_freq without deprecation warning.
875 """
876 if self.ndim != 1:
877 return None
878 try:
879 return frequencies.infer_freq_str(self)
880 except ValueError:
881 return None
882
883 @property # NB: override with cache_readonly in immutable subclasses
884 def inferred_freq(self) -> str | BaseOffset | None:
885 """
886 Tries to return a string representing a frequency generated by infer_freq.
887
888 .. deprecated:: 3.1.0
889 A future version of pandas will return a :class:`BaseOffset` instead of
890 a string. Use
891 ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt
892 in to the future behavior.
893
894 Returns None if it can't autodetect the frequency.
895
896 See Also
897 --------
898 DatetimeIndex.freqstr : Return the frequency object as a string if it's set,
899 otherwise None.
900
901 Examples
902 --------
903 For DatetimeIndex:
904
905 >>> idx = pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"])
906 >>> idx.inferred_freq # doctest: +SKIP
907 '2D'
908
909 For TimedeltaIndex:
910
911 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"])
912 >>> tdelta_idx
913 TimedeltaIndex(['0 days', '10 days', '20 days'],
914 dtype='timedelta64[us]', freq=None)
915 >>> tdelta_idx.inferred_freq # doctest: +SKIP
916 '10D'
917 """
918 return frequencies.maybe_convert_inferred_freq(
919 self._inferred_freq_str, "inferred_freq"
920 )
921
922 # monotonicity/uniqueness properties are called via frequencies.infer_freq,
923 # see GH#23789
924
925 @property
926 def _is_monotonic_increasing(self) -> bool:
927 return algos.is_monotonic(self.asi8, timelike=True)[0]
928
929 @property
930 def _is_monotonic_decreasing(self) -> bool:
931 return algos.is_monotonic(self.asi8, timelike=True)[1]
932
933 @property
934 def _is_unique(self) -> bool:
935 return len(unique1d(self.asi8.ravel("K"))) == self.size
936
937 # ------------------------------------------------------------------
938 # Arithmetic Methods
939
940 def _cmp_method(self, other, op):
941 if self.ndim > 1 and getattr(other, "shape", None) == self.shape:
942 # TODO: handle 2D-like listlikes
943 return op(self.ravel(), other.ravel()).reshape(self.shape)
944
945 if is_list_like(other):
946 if not isinstance(
947 other, (list, np.ndarray, ExtensionArray)
948 ) and not ops.has_castable_attr(other):
949 warnings.warn(
950 f"Operation with {type(other).__name__} is deprecated. "
951 "In a future version these will be treated as scalar-like. "
952 "To retain the old behavior, explicitly wrap in a Series "
953 "instead.",
954 Pandas4Warning,
955 stacklevel=find_stack_level(),
956 )
957
958 try:
959 other = self._validate_comparison_value(other)
960 except InvalidComparison:
961 if hasattr(other, "dtype") and isinstance(other.dtype, ArrowDtype):
962 return NotImplemented
963 return invalid_comparison(self, other, op)
964
965 dtype = getattr(other, "dtype", None)
966 if is_object_dtype(dtype):
967 # We have to use comp_method_OBJECT_ARRAY instead of numpy
968 # comparison otherwise it would raise when comparing to None
969 result = ops.comp_method_OBJECT_ARRAY(
970 op, np.asarray(self.astype(object)), other
971 )
972 return result
973 if other is NaT:
974 if op is operator.ne:
975 result = np.ones(self.shape, dtype=bool)
976 else:
977 result = np.zeros(self.shape, dtype=bool)
978 return result
979
980 if not isinstance(self.dtype, PeriodDtype):
981 self = cast("TimelikeOps", self)
982 if self._creso != other._creso:
983 if not isinstance(other, type(self)):
984 # i.e. Timedelta/Timestamp, cast to ndarray and let
985 # compare_mismatched_resolutions handle broadcasting
986 try:
987 # GH#52080 see if we can losslessly cast to shared unit
988 other = other.as_unit(self.unit, round_ok=False)
989 except ValueError:
990 other_arr = np.array(other.asm8)
991 return compare_mismatched_resolutions(
992 self._ndarray, other_arr, op
993 )
994 else:
995 other_arr = other._ndarray
996 return compare_mismatched_resolutions(self._ndarray, other_arr, op)
997
998 other_vals = self._unbox(other)
999 # GH#37462 comparison on i8 values is almost 2x faster than M8/m8
1000 result = op(self._ndarray.view("i8"), other_vals.view("i8"))
1001
1002 o_mask = isna(other)
1003 mask = self._isnan | o_mask
1004 if mask.any():
1005 nat_result = op is operator.ne
1006 np.putmask(result, mask, nat_result)
1007
1008 return result
1009
1010 # pow is invalid for all three subclasses; TimedeltaArray will override
1011 # the multiplication and division ops
1012 __pow__ = _make_unpacked_invalid_op("__pow__")
1013 __rpow__ = _make_unpacked_invalid_op("__rpow__")
1014 __mul__ = _make_unpacked_invalid_op("__mul__")
1015 __rmul__ = _make_unpacked_invalid_op("__rmul__")
1016 __truediv__ = _make_unpacked_invalid_op("__truediv__")
1017 __rtruediv__ = _make_unpacked_invalid_op("__rtruediv__")
1018 __floordiv__ = _make_unpacked_invalid_op("__floordiv__")
1019 __rfloordiv__ = _make_unpacked_invalid_op("__rfloordiv__")
1020 __mod__ = _make_unpacked_invalid_op("__mod__")
1021 __rmod__ = _make_unpacked_invalid_op("__rmod__")
1022 __divmod__ = _make_unpacked_invalid_op("__divmod__")
1023 __rdivmod__ = _make_unpacked_invalid_op("__rdivmod__")
1024
1025 @final
1026 def _get_i8_values_and_mask(
1027 self, other
1028 ) -> tuple[int | npt.NDArray[np.int64], npt.NDArray[np.bool_] | None]:
1029 """
1030 Get the int64 values and b_mask to pass to add_overflowsafe.
1031 """
1032 if isinstance(other, Period):
1033 i8values = other.ordinal
1034 mask = None
1035 elif isinstance(other, (Timestamp, Timedelta)):
1036 i8values = other._value
1037 mask = None
1038 else:
1039 # PeriodArray, DatetimeArray, TimedeltaArray
1040 mask = other._isnan
1041 i8values = other.asi8
1042 return i8values, mask
1043
1044 @final
1045 def _add_datetimelike_scalar(self, other) -> DatetimeArray:
1046 if not lib.is_np_dtype(self.dtype, "m"):
1047 raise TypeError(
1048 f"cannot add {type(self).__name__} and {type(other).__name__}"
1049 )
1050
1051 self = cast("TimedeltaArray", self)
1052
1053 from pandas.core.arrays import DatetimeArray
1054 from pandas.core.arrays.datetimes import tz_to_dtype
1055
1056 assert other is not NaT
1057 if isna(other):
1058 # i.e. np.datetime64("NaT")
1059 # In this case we specifically interpret NaT as a datetime, not
1060 # the timedelta interpretation we would get by returning self + NaT
1061 result = self._ndarray + NaT.to_datetime64().astype(f"M8[{self.unit}]")
1062 # Preserve our resolution
1063 return DatetimeArray._simple_new(result, dtype=result.dtype)
1064
1065 other = Timestamp(other)
1066 self, other = self._ensure_matching_resos(other)
1067 self = cast("TimedeltaArray", self)
1068
1069 other_i8, o_mask = self._get_i8_values_and_mask(other)
1070 result = add_overflowsafe(self.asi8, np.asarray(other_i8, dtype="i8"))
1071 res_values = result.view(f"M8[{self.unit}]")
1072
1073 dtype = tz_to_dtype(tz=other.tz, unit=self.unit)
1074 res_values = result.view(f"M8[{self.unit}]")
1075 return DatetimeArray._simple_new(res_values, dtype=dtype)
1076
1077 @final
1078 def _add_datetime_arraylike(self, other: DatetimeArray) -> DatetimeArray:
1079 if not lib.is_np_dtype(self.dtype, "m"):
1080 raise TypeError(
1081 f"cannot add {type(self).__name__} and {type(other).__name__}"
1082 )
1083
1084 # defer to DatetimeArray.__add__
1085 return other + self
1086
1087 @final
1088 def _sub_datetimelike_scalar(
1089 self, other: datetime | np.datetime64
1090 ) -> TimedeltaArray:
1091 if self.dtype.kind != "M":
1092 raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}")
1093
1094 self = cast("DatetimeArray", self)
1095 # subtract a datetime from myself, yielding an ndarray[timedelta64[ns]]
1096
1097 if isna(other):
1098 # i.e. np.datetime64("NaT")
1099 return self - NaT
1100
1101 ts = Timestamp(other)
1102
1103 self, ts = self._ensure_matching_resos(ts)
1104 return self._sub_datetimelike(ts)
1105
1106 @final
1107 def _sub_datetime_arraylike(self, other: DatetimeArray) -> TimedeltaArray:
1108 if self.dtype.kind != "M":
1109 raise TypeError(f"cannot subtract a datelike from a {type(self).__name__}")
1110
1111 if len(self) != len(other):
1112 raise ValueError("cannot add indices of unequal length")
1113
1114 self = cast("DatetimeArray", self)
1115
1116 self, other = self._ensure_matching_resos(other)
1117 return self._sub_datetimelike(other)
1118
1119 @final
1120 def _sub_datetimelike(self, other: Timestamp | DatetimeArray) -> TimedeltaArray:
1121 self = cast("DatetimeArray", self)
1122
1123 from pandas.core.arrays import TimedeltaArray
1124
1125 try:
1126 self._assert_tzawareness_compat(other)
1127 except TypeError as err:
1128 new_message = str(err).replace("compare", "subtract")
1129 raise type(err)(new_message) from err
1130
1131 other_i8, o_mask = self._get_i8_values_and_mask(other)
1132 res_values = add_overflowsafe(self.asi8, np.asarray(-other_i8, dtype="i8"))
1133 res_m8 = res_values.view(f"timedelta64[{self.unit}]")
1134
1135 return TimedeltaArray._simple_new(res_m8, dtype=res_m8.dtype)
1136
1137 @final
1138 def _add_period(self, other: Period) -> PeriodArray:
1139 if not lib.is_np_dtype(self.dtype, "m"):
1140 raise TypeError(f"cannot add Period to a {type(self).__name__}")
1141
1142 # We will wrap in a PeriodArray and defer to the reversed operation
1143 from pandas.core.arrays.period import PeriodArray
1144
1145 i8vals = np.broadcast_to(other.ordinal, self.shape)
1146 dtype = PeriodDtype(other.freq)
1147 parr = PeriodArray(i8vals, dtype=dtype)
1148 return parr + self
1149
1150 def _add_offset(self, offset):
1151 raise AbstractMethodError(self)
1152
1153 def _add_timedeltalike_scalar(self, other):
1154 """
1155 Add a delta of a timedeltalike
1156
1157 Returns
1158 -------
1159 Same type as self
1160 """
1161 if isna(other):
1162 # i.e np.timedelta64("NaT")
1163 new_values = np.empty(self.shape, dtype="i8").view(self._ndarray.dtype)
1164 new_values.fill(iNaT)
1165 return type(self)._simple_new(new_values, dtype=self.dtype)
1166
1167 # PeriodArray overrides, so we only get here with DTA/TDA
1168 self = cast("DatetimeArray | TimedeltaArray", self)
1169 other = Timedelta(other)
1170 self, other = self._ensure_matching_resos(other)
1171 return self._add_timedeltalike(other)
1172
1173 def _add_timedelta_arraylike(self, other: TimedeltaArray) -> Self:
1174 """
1175 Add a delta of a TimedeltaIndex
1176
1177 Returns
1178 -------
1179 Same type as self
1180 """
1181 # overridden by PeriodArray
1182
1183 if len(self) != len(other):
1184 raise ValueError("cannot add indices of unequal length")
1185
1186 self, other = cast(
1187 "DatetimeArray | TimedeltaArray", self
1188 )._ensure_matching_resos(other)
1189 return self._add_timedeltalike(other)
1190
1191 @final
1192 def _add_timedeltalike(self, other: Timedelta | TimedeltaArray) -> Self:
1193 other_i8, o_mask = self._get_i8_values_and_mask(other)
1194 new_values = add_overflowsafe(self.asi8, np.asarray(other_i8, dtype="i8"))
1195 res_values = new_values.view(self._ndarray.dtype)
1196
1197 return type(self)._simple_new(res_values, dtype=self.dtype)
1198
1199 @final
1200 def _add_nat(self) -> Self:
1201 """
1202 Add pd.NaT to self
1203 """
1204 if isinstance(self.dtype, PeriodDtype):
1205 raise TypeError(
1206 f"Cannot add {type(self).__name__} and {type(NaT).__name__}"
1207 )
1208
1209 # GH#19124 pd.NaT is treated like a timedelta for both timedelta
1210 # and datetime dtypes
1211 result = np.empty(self.shape, dtype=np.int64)
1212 result.fill(iNaT)
1213 result = result.view(self._ndarray.dtype) # preserve reso
1214 return type(self)._simple_new(result, dtype=self.dtype)
1215
1216 @final
1217 def _sub_nat(self) -> np.ndarray:
1218 """
1219 Subtract pd.NaT from self
1220 """
1221 # GH#19124 Timedelta - datetime is not in general well-defined.
1222 # We make an exception for pd.NaT, which in this case quacks
1223 # like a timedelta.
1224 # For datetime64 dtypes by convention we treat NaT as a datetime, so
1225 # this subtraction returns a timedelta64 dtype.
1226 # For period dtype, timedelta64 is a close-enough return dtype.
1227 result = np.empty(self.shape, dtype=np.int64)
1228 result.fill(iNaT)
1229 if self.dtype.kind in "mM":
1230 # We can retain unit in dtype
1231 self = cast("DatetimeArray| TimedeltaArray", self)
1232 return result.view(f"timedelta64[{self.unit}]")
1233 else:
1234 return result.view("timedelta64[ns]")
1235
1236 @final
1237 def _sub_periodlike(self, other: Period | PeriodArray) -> npt.NDArray[np.object_]:
1238 # If the operation is well-defined, we return an object-dtype ndarray
1239 # of DateOffsets. Null entries are filled with pd.NaT
1240 if not isinstance(self.dtype, PeriodDtype):
1241 raise TypeError(
1242 f"cannot subtract {type(other).__name__} from {type(self).__name__}"
1243 )
1244
1245 self = cast("PeriodArray", self)
1246 self._check_compatible_with(other)
1247
1248 other_i8, o_mask = self._get_i8_values_and_mask(other)
1249 # GH#66552 the difference is a count of periods, not an ordinal, so
1250 # INT64_MIN is a legitimate answer here and not the NaT sentinel.
1251 new_i8_data = add_overflowsafe(
1252 self.asi8, np.asarray(-other_i8, dtype="i8"), sentinel_ok=True
1253 )
1254 # multiply by python ints: numpy's scalar multiply spuriously reports
1255 # overflow for a np.int64 count of INT64_MIN on Windows
1256 counts = new_i8_data.ravel().tolist()
1257 # dtype=object is necessary for the length-zero case, where numpy
1258 # would otherwise infer float64 (GH#40624)
1259 new_data = np.array(
1260 [self.freq.base * count for count in counts], dtype=object
1261 ).reshape(new_i8_data.shape)
1262
1263 if o_mask is None:
1264 # i.e. Period scalar
1265 mask = self._isnan
1266 else:
1267 # i.e. PeriodArray
1268 mask = self._isnan | o_mask
1269 new_data[mask] = NaT
1270 return new_data
1271
1272 @final
1273 def _addsub_object_array(self, other: npt.NDArray[np.object_], op) -> np.ndarray:
1274 """
1275 Add or subtract array-like of DateOffset objects
1276
1277 Parameters
1278 ----------
1279 other : np.ndarray[object]
1280 op : {operator.add, operator.sub}
1281
1282 Returns
1283 -------
1284 np.ndarray[object]
1285 Except in fastpath case with length 1 where we operate on the
1286 contained scalar.
1287 """
1288 assert op in [operator.add, operator.sub]
1289 if len(other) == 1 and self.ndim == 1:
1290 # Note: without this special case, we could annotate return type
1291 # as ndarray[object]
1292 # If both 1D then broadcasting is unambiguous
1293 return op(self, other[0])
1294
1295 if config["mode"]["performance_warnings"]:
1296 warnings.warn(
1297 "Adding/subtracting object-dtype array to "
1298 f"{type(self).__name__} not vectorized.",
1299 PerformanceWarning,
1300 stacklevel=find_stack_level(),
1301 )
1302
1303 # Caller is responsible for broadcasting if necessary
1304 assert self.shape == other.shape, (self.shape, other.shape)
1305
1306 res_values = op(self.astype("O"), np.asarray(other))
1307 return res_values
1308
1309 def _accumulate(self, name: str, *, skipna: bool = True, **kwargs) -> Self:
1310 if name not in {"cummin", "cummax"}:
1311 raise TypeError(f"Accumulation {name} not supported for {type(self)}")
1312
1313 op = getattr(datetimelike_accumulations, name)
1314 result = op(self.copy(), skipna=skipna, **kwargs)
1315
1316 return type(self)._simple_new(result, dtype=self.dtype)
1317
1318 @unpack_zerodim_and_defer("__add__")
1319 def __add__(self, other):
1320 other_dtype = getattr(other, "dtype", None)
1321 other = ensure_wrapped_if_datetimelike(other)
1322
1323 # scalar others
1324 if other is NaT:
1325 result: np.ndarray | DatetimeLikeArrayMixin = self._add_nat()
1326 elif isinstance(other, (Tick, timedelta, np.timedelta64)):
1327 result = self._add_timedeltalike_scalar(other)
1328 elif isinstance(other, Day) and lib.is_np_dtype(self.dtype, "Mm"):
1329 # We treat this as Tick-like
1330 td = Timedelta(days=other.n).as_unit("s")
1331 result = self._add_timedeltalike_scalar(td)
1332 elif isinstance(other, BaseOffset):
1333 # specifically _not_ a Tick
1334 result = self._add_offset(other)
1335 elif isinstance(other, (datetime, np.datetime64)):
1336 result = self._add_datetimelike_scalar(other)
1337 elif isinstance(other, Period) and lib.is_np_dtype(self.dtype, "m"):
1338 result = self._add_period(other)
1339 elif lib.is_integer(other):
1340 # This check must come after the check for np.timedelta64
1341 # as is_integer returns True for these
1342 if not isinstance(self.dtype, PeriodDtype):
1343 raise integer_op_not_supported(self)
1344 obj = cast("PeriodArray", self)
1345 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.add)
1346
1347 # array-like others
1348 elif lib.is_np_dtype(other_dtype, "m"):
1349 # TimedeltaIndex, ndarray[timedelta64]
1350 result = self._add_timedelta_arraylike(other)
1351 elif is_object_dtype(other_dtype):
1352 # e.g. Array/Index of DateOffset objects
1353 result = self._addsub_object_array(other, operator.add)
1354 elif lib.is_np_dtype(other_dtype, "M") or isinstance(
1355 other_dtype, DatetimeTZDtype
1356 ):
1357 # DatetimeIndex, ndarray[datetime64]
1358 return self._add_datetime_arraylike(other)
1359 elif is_integer_dtype(other_dtype):
1360 if not isinstance(self.dtype, PeriodDtype):
1361 raise integer_op_not_supported(self)
1362 obj = cast("PeriodArray", self)
1363 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.add)
1364 elif other_dtype is not None and other_dtype.kind == "b":
1365 # GH#68452 is_integer_dtype is False for bool, so without this bool
1366 # operands fall through to numpy, which reads True as a one-unit
1367 # timedelta. Period, where ints are legal, keeps its object-path raise
1368 if isinstance(self.dtype, PeriodDtype):
1369 return NotImplemented
1370 raise integer_op_not_supported(self)
1371 else:
1372 # Includes Categorical, other ExtensionArrays
1373 # For PeriodDtype, if self is a TimedeltaArray and other is a
1374 # PeriodArray with a timedelta-like (i.e. Tick) freq, this
1375 # operation is valid. Defer to the PeriodArray implementation.
1376 # In remaining cases, this will end up raising TypeError.
1377 return NotImplemented
1378
1379 if isinstance(result, np.ndarray) and lib.is_np_dtype(result.dtype, "m"):
1380 from pandas.core.arrays import TimedeltaArray
1381
1382 return TimedeltaArray._from_sequence(result, dtype=result.dtype)
1383 return result
1384
1385 def __radd__(self, other):
1386 # alias for __add__
1387 return self.__add__(other)
1388
1389 @unpack_zerodim_and_defer("__sub__")
1390 def __sub__(self, other):
1391 other_dtype = getattr(other, "dtype", None)
1392 other = ensure_wrapped_if_datetimelike(other)
1393
1394 # scalar others
1395 if other is NaT:
1396 result: np.ndarray | DatetimeLikeArrayMixin = self._sub_nat()
1397 elif isinstance(other, (Tick, timedelta, np.timedelta64)):
1398 result = self._add_timedeltalike_scalar(-other)
1399 elif isinstance(other, Day) and lib.is_np_dtype(self.dtype, "Mm"):
1400 # We treat this as Tick-like
1401 td = Timedelta(days=other.n).as_unit("s")
1402 result = self._add_timedeltalike_scalar(-td)
1403 elif isinstance(other, BaseOffset):
1404 # specifically _not_ a Tick
1405 result = self._add_offset(-other)
1406 elif isinstance(other, (datetime, np.datetime64)):
1407 result = self._sub_datetimelike_scalar(other)
1408 elif lib.is_integer(other):
1409 # This check must come after the check for np.timedelta64
1410 # as is_integer returns True for these
1411 if not isinstance(self.dtype, PeriodDtype):
1412 raise integer_op_not_supported(self)
1413 obj = cast("PeriodArray", self)
1414 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.sub)
1415
1416 elif isinstance(other, Period):
1417 result = self._sub_periodlike(other)
1418
1419 # array-like others
1420 elif lib.is_np_dtype(other_dtype, "m"):
1421 # TimedeltaIndex, ndarray[timedelta64]
1422 result = self._add_timedelta_arraylike(-other)
1423 elif is_object_dtype(other_dtype):
1424 # e.g. Array/Index of DateOffset objects
1425 result = self._addsub_object_array(other, operator.sub)
1426 elif lib.is_np_dtype(other_dtype, "M") or isinstance(
1427 other_dtype, DatetimeTZDtype
1428 ):
1429 # DatetimeIndex, ndarray[datetime64]
1430 result = self._sub_datetime_arraylike(other)
1431 elif isinstance(other_dtype, PeriodDtype):
1432 # PeriodIndex
1433 result = self._sub_periodlike(other)
1434 elif is_integer_dtype(other_dtype):
1435 if not isinstance(self.dtype, PeriodDtype):
1436 raise integer_op_not_supported(self)
1437 obj = cast("PeriodArray", self)
1438 result = obj._addsub_int_array_or_scalar(other * obj.dtype._n, operator.sub)
1439 elif other_dtype is not None and other_dtype.kind == "b":
1440 # GH#68452, see __add__
1441 if isinstance(self.dtype, PeriodDtype):
1442 return NotImplemented
1443 raise integer_op_not_supported(self)
1444 else:
1445 # Includes ExtensionArrays, float_dtype
1446 return NotImplemented
1447
1448 if isinstance(result, np.ndarray) and lib.is_np_dtype(result.dtype, "m"):
1449 from pandas.core.arrays import TimedeltaArray
1450
1451 return TimedeltaArray._from_sequence(result, dtype=result.dtype)
1452 return result
1453
1454 def __rsub__(self, other):
1455 other_dtype = getattr(other, "dtype", None)
1456 other_is_dt64 = lib.is_np_dtype(other_dtype, "M") or isinstance(
1457 other_dtype, DatetimeTZDtype
1458 )
1459
1460 if other_is_dt64 and lib.is_np_dtype(self.dtype, "m"):
1461 # ndarray[datetime64] cannot be subtracted from self, so
1462 # we need to wrap in DatetimeArray/Index and flip the operation
1463 if lib.is_scalar(other):
1464 # i.e. np.datetime64 object
1465 return Timestamp(other) - self
1466 if not isinstance(other, DatetimeLikeArrayMixin):
1467 # Avoid down-casting DatetimeIndex
1468 from pandas.core.arrays import DatetimeArray
1469
1470 other = DatetimeArray._from_sequence(other, dtype=other.dtype)
1471 return other - self
1472 elif self.dtype.kind == "M" and hasattr(other, "dtype") and not other_is_dt64:
1473 # GH#19959 datetime - datetime is well-defined as timedelta,
1474 # but any other type - datetime is not well-defined.
1475 raise TypeError(
1476 f"cannot subtract {type(self).__name__} from "
1477 f"{type(other).__name__}[{other.dtype}]"
1478 )
1479 elif isinstance(self.dtype, PeriodDtype) and lib.is_np_dtype(other_dtype, "m"):
1480 # TODO: Can we simplify/generalize these cases at all?
1481 raise TypeError(f"cannot subtract {type(self).__name__} from {other.dtype}")
1482 elif lib.is_np_dtype(self.dtype, "m"):
1483 self = cast("TimedeltaArray", self)
1484 return (-self) + other
1485
1486 flipped = self - other
1487 if flipped.dtype.kind == "M":
1488 # GH#59571 give a more helpful exception message
1489 raise TypeError(
1490 f"cannot subtract {type(self).__name__} from {type(other).__name__}"
1491 )
1492 # We get here with e.g. datetime objects
1493 return -flipped
1494
1495 def __iadd__(self, other) -> Self:
1496 result = self + other
1497 self[:] = result[:]
1498 return self
1499
1500 def __isub__(self, other) -> Self:
1501 result = self - other
1502 self[:] = result[:]
1503 return self
1504
1505 # --------------------------------------------------------------
1506 # Reductions
1507
1508 @_period_dispatch
1509 def _quantile(
1510 self,
1511 qs: npt.NDArray[np.float64],
1512 interpolation: str,
1513 ) -> Self:
1514 return super()._quantile(qs=qs, interpolation=interpolation)
1515
1516 @_period_dispatch
1517 def min(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs):
1518 """
1519 Return the minimum value of the Array or minimum along
1520 an axis.
1521
1522 See Also
1523 --------
1524 numpy.ndarray.min
1525 Index.min : Return the minimum value in an Index.
1526 Series.min : Return the minimum value in a Series.
1527 """
1528 nv.validate_min((), kwargs)
1529 nv.validate_minmax_axis(axis, self.ndim)
1530
1531 result = nanops.nanmin(self._ndarray, axis=axis, skipna=skipna)
1532 return self._wrap_reduction_result(axis, result)
1533
1534 @_period_dispatch
1535 def max(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs):
1536 """
1537 Return the maximum value of the Array or maximum along
1538 an axis.
1539
1540 See Also
1541 --------
1542 numpy.ndarray.max
1543 Index.max : Return the maximum value in an Index.
1544 Series.max : Return the maximum value in a Series.
1545 """
1546 nv.validate_max((), kwargs)
1547 nv.validate_minmax_axis(axis, self.ndim)
1548
1549 result = nanops.nanmax(self._ndarray, axis=axis, skipna=skipna)
1550 return self._wrap_reduction_result(axis, result)
1551
1552 def mean(self, *, skipna: bool = True, axis: AxisInt | None = 0):
1553 """
1554 Return the mean value of the Array.
1555
1556 Parameters
1557 ----------
1558 skipna : bool, default True
1559 Whether to ignore any NaT elements.
1560 axis : int, optional, default 0
1561 Axis for the function to be applied on.
1562
1563 Returns
1564 -------
1565 scalar
1566 Timestamp or Timedelta.
1567
1568 See Also
1569 --------
1570 numpy.ndarray.mean : Returns the average of array elements along a given axis.
1571 Series.mean : Return the mean value in a Series.
1572
1573 Notes
1574 -----
1575 mean is only defined for Datetime and Timedelta dtypes, not for Period.
1576
1577 Examples
1578 --------
1579 For :class:`pandas.DatetimeIndex`:
1580
1581 >>> idx = pd.date_range("2001-01-01 00:00", periods=3)
1582 >>> idx
1583 DatetimeIndex(['2001-01-01', '2001-01-02', '2001-01-03'],
1584 dtype='datetime64[us]', freq='D')
1585 >>> idx.mean()
1586 Timestamp('2001-01-02 00:00:00')
1587
1588 For :class:`pandas.TimedeltaIndex`:
1589
1590 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
1591 >>> tdelta_idx
1592 TimedeltaIndex(['1 days', '2 days', '3 days'],
1593 dtype='timedelta64[us]', freq=None)
1594 >>> tdelta_idx.mean()
1595 Timedelta('2 days 00:00:00')
1596 """
1597 if isinstance(self.dtype, PeriodDtype):
1598 # See discussion in GH#24757
1599 raise TypeError(
1600 f"mean is not implemented for {type(self).__name__} since the "
1601 "meaning is ambiguous. An alternative is "
1602 "obj.to_timestamp(how='start').mean()"
1603 )
1604
1605 result = nanops.nanmean(
1606 self._ndarray, axis=axis, skipna=skipna, mask=self.isna()
1607 )
1608 return self._wrap_reduction_result(axis, result)
1609
1610 @_period_dispatch
1611 def median(self, *, axis: AxisInt | None = None, skipna: bool = True, **kwargs):
1612 nv.validate_median((), kwargs)
1613
1614 if axis is not None and abs(axis) >= self.ndim:
1615 raise ValueError("abs(axis) must be less than ndim")
1616
1617 result = nanops.nanmedian(self._ndarray, axis=axis, skipna=skipna)
1618 return self._wrap_reduction_result(axis, result)
1619
1620 def _mode(self, dropna: bool = True):
1621 mask = None
1622 if dropna:
1623 mask = self.isna()
1624
1625 i8modes, _ = algorithms.mode(self.view("i8"), mask=mask)
1626 npmodes = i8modes.view(self._ndarray.dtype)
1627 npmodes = cast("np.ndarray", npmodes)
1628 return self._from_backing_data(npmodes)
1629
1630 # ------------------------------------------------------------------
1631 # GroupBy Methods
1632
1633 def _groupby_op(
1634 self,
1635 *,
1636 how: str,
1637 has_dropped_na: bool,
1638 min_count: int,
1639 ngroups: int,
1640 ids: npt.NDArray[np.intp],
1641 **kwargs,
1642 ):
1643 dtype = self.dtype
1644 if dtype.kind == "M":
1645 # Adding/multiplying datetimes is not valid
1646 if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]:
1647 raise TypeError(f"datetime64 type does not support operation '{how}'")
1648 if how in ["any", "all"]:
1649 # GH#34479
1650 raise TypeError(nanops.dt64_any_all_msg(how))
1651
1652 elif isinstance(dtype, PeriodDtype):
1653 # Adding/multiplying Periods is not valid
1654 if how in ["sum", "prod", "cumsum", "cumprod", "var", "skew", "kurt"]:
1655 raise TypeError(f"Period type does not support {how} operations")
1656 if how in ["any", "all"]:
1657 # GH#34479
1658 raise TypeError(
1659 f"'{how}' with PeriodDtype is no longer supported. "
1660 f"Use (obj != pd.Period(ordinal=0, freq=freq)).{how}() instead."
1661 )
1662 # timedeltas we can add but not multiply
1663 elif how in ["prod", "cumprod", "skew", "kurt", "var"]:
1664 raise TypeError(f"timedelta64 type does not support {how} operations")
1665
1666 # All of the functions implemented here are ordinal, so we can
1667 # operate on the tz-naive equivalents
1668 npvalues = self._ndarray.view("M8[ns]")
1669
1670 from pandas.core.groupby.ops import WrappedCythonOp
1671
1672 op = WrappedCythonOp(how=how, has_dropped_na=has_dropped_na)
1673
1674 res_values = op._cython_op_ndim_compat(
1675 npvalues,
1676 min_count=min_count,
1677 ngroups=ngroups,
1678 comp_ids=ids,
1679 mask=None,
1680 **kwargs,
1681 )
1682
1683 if op.how in op.cast_blocklist:
1684 # i.e. how in ["rank"], since other cast_blocklist methods don't go
1685 # through cython_operation
1686 return res_values
1687
1688 # We did a view to M8[ns] above, now we go the other direction
1689 assert res_values.dtype == "M8[ns]"
1690 if how in ["std", "sem"]:
1691 from pandas.core.arrays import TimedeltaArray
1692
1693 if isinstance(self.dtype, PeriodDtype):
1694 raise TypeError("'std' and 'sem' are not valid for PeriodDtype")
1695 self = cast("DatetimeArray | TimedeltaArray", self)
1696 new_dtype = f"m8[{self.unit}]"
1697 res_values = res_values.view(new_dtype)
1698 return TimedeltaArray._simple_new(res_values, dtype=res_values.dtype)
1699
1700 res_values = res_values.view(self._ndarray.dtype)
1701 return self._from_backing_data(res_values)
1702
1703 def _groupby_quantile(
1704 self,
1705 *,
1706 qs: npt.NDArray[np.float64],
1707 interpolation: Literal["linear", "lower", "higher", "nearest", "midpoint"],
1708 ids: npt.NDArray[np.intp],
1709 ngroups: int,
1710 starts: npt.NDArray[np.int64],
1711 ends: npt.NDArray[np.int64],
1712 ) -> ArrayLike:
1713 nqs = len(qs)
1714 mask = self.isna()
1715 i8vals = self.asi8
1716
1717 # Block manager may store DatetimeArray/TimedeltaArray as 2D (1, N)
1718 ncols = 1
1719 if self.ndim == 2:
1720 ncols = self._ndarray.shape[0]
1721
1722 out = np.empty((ncols, ngroups, nqs), dtype=np.float64)
1723 for col in range(ncols):
1724 if self.ndim == 2:
1725 col_mask = mask[col]
1726 col_vals = i8vals[col]
1727 else:
1728 col_mask = mask
1729 col_vals = i8vals
1730
1731 libgroupby.group_quantile(
1732 out[col],
1733 values=col_vals,
1734 mask=col_mask,
1735 labels=ids,
1736 qs=qs,
1737 interpolation=interpolation,
1738 starts=starts,
1739 ends=ends,
1740 result_mask=None,
1741 is_datetimelike=True,
1742 )
1743
1744 if self.ndim == 1:
1745 out = out.ravel("K") # type: ignore[assignment]
1746 else:
1747 out = out.reshape(ncols, ngroups * nqs) # type: ignore[assignment]
1748
1749 # All-NA groups come back as NaN; casting NaN to i8 is platform
1750 # dependent (0 on some architectures), so map them to iNaT explicitly.
1751 na_mask = np.isnan(out)
1752 out[na_mask] = 0
1753 out = out.astype("i8")
1754 out[na_mask] = iNaT
1755 out = out.view(self._ndarray.dtype)
1756 return self._from_backing_data(out)
1757
1758
1759class DatelikeOps(DatetimeLikeArrayMixin):
1760 """
1761 Common ops for DatetimeIndex/PeriodIndex, but not TimedeltaIndex.
1762 """
1763
1764 def strftime(self, date_format: str) -> npt.NDArray[np.object_]:
1765 """
1766 Convert to Index using specified date_format.
1767
1768 Return an Index of formatted strings specified by date_format, which
1769 supports the same string format as the python standard library. Details
1770 of the string format can be found in `python string format
1771 doc <https://docs.python.org/3/library/datetime.html
1772 #strftime-and-strptime-behavior>`__.
1773
1774 Formats supported by the C `strftime` API but not by the python string format
1775 doc (such as `"%R"`, `"%r"`) are not officially supported and should be
1776 preferably replaced with their supported equivalents (such as `"%H:%M"`,
1777 `"%I:%M:%S %p"`).
1778
1779 Note that `PeriodIndex` support additional directives, detailed in
1780 `Period.strftime`.
1781
1782 Parameters
1783 ----------
1784 date_format : str
1785 Date format string (e.g. "%%Y-%%m-%%d").
1786
1787 Returns
1788 -------
1789 ndarray[object]
1790 NumPy ndarray of formatted strings.
1791
1792 See Also
1793 --------
1794 to_datetime : Convert the given argument to datetime.
1795 DatetimeIndex.normalize : Return DatetimeIndex with times to midnight.
1796 DatetimeIndex.round : Round the DatetimeIndex to the specified freq.
1797 DatetimeIndex.floor : Floor the DatetimeIndex to the specified freq.
1798 Timestamp.strftime : Format a single Timestamp.
1799 Period.strftime : Format a single Period.
1800
1801 Examples
1802 --------
1803 >>> rng = pd.date_range(pd.Timestamp("2018-03-10 09:00"), periods=3, freq="s")
1804 >>> rng.strftime("%B %d, %Y, %r")
1805 Index(['March 10, 2018, 09:00:00 AM', 'March 10, 2018, 09:00:01 AM',
1806 'March 10, 2018, 09:00:02 AM'],
1807 dtype='str')
1808 """
1809 result = self._format_native_types(date_format=date_format, na_rep=np.nan)
1810 if using_string_dtype():
1811 from pandas import StringDtype
1812
1813 return pd_array(result, dtype=StringDtype(na_value=np.nan)) # type: ignore[return-value]
1814 return result.astype(object, copy=False)
1815
1816
1817_ITER_CHUNKSIZE = 10_000
1818
1819
1820class TimelikeOps(DatetimeLikeArrayMixin):
1821 """
1822 Common ops for TimedeltaIndex/DatetimeIndex, but not PeriodIndex.
1823 """
1824
1825 @classmethod
1826 def _validate_dtype(cls, values, dtype):
1827 raise AbstractMethodError(cls)
1828
1829 def _iter_convert_chunk(self, data: np.ndarray) -> np.ndarray:
1830 raise AbstractMethodError(self)
1831
1832 def __iter__(self) -> Iterator[Any]:
1833 if self.ndim > 1:
1834 for i in range(len(self)):
1835 yield self[i]
1836 else:
1837 # convert in chunks of 10k for efficiency
1838 data = self._ndarray
1839 length = len(self)
1840 chunksize = _ITER_CHUNKSIZE
1841 chunks = (length // chunksize) + 1
1842 for i in range(chunks):
1843 start_i = i * chunksize
1844 end_i = min((i + 1) * chunksize, length)
1845 yield from self._iter_convert_chunk(data[start_i:end_i])
1846
1847 @final
1848 @classmethod
1849 def _validate_frequency(cls, index, freq: BaseOffset) -> None:
1850 """
1851 Validate that a frequency is compatible with the values of a given
1852 Datetime Array/Index or Timedelta Array/Index
1853
1854 Parameters
1855 ----------
1856 index : DatetimeIndex or TimedeltaIndex
1857 The index on which to determine if the given frequency is valid
1858 freq : DateOffset
1859 The frequency to validate
1860 """
1861 inferred = index._inferred_freq_str
1862 if index.size == 0 or inferred == freq.freqstr:
1863 return None
1864
1865 if getattr(index.dtype, "tz", None) is not None and not isinstance(freq, Tick):
1866 # GH#55499 non-tick offsets do wall-time arithmetic, so validate
1867 # against wall times; avoids raising on ambiguous times.
1868 index = index.tz_localize(None)
1869
1870 try:
1871 on_freq = cls._generate_range(
1872 start=index[0],
1873 end=None,
1874 periods=len(index),
1875 freq=freq,
1876 unit=index.unit,
1877 )
1878 if not lib.array_equivalent_bytes(index.asi8, on_freq.asi8):
1879 raise ValueError
1880 except ValueError as err:
1881 if "non-fixed" in str(err):
1882 # non-fixed frequencies are not meaningful for timedelta64;
1883 # we retain that error message
1884 raise err
1885 # GH#11587 the main way this is reached is if the `np.array_equal`
1886 # check above is False. This can also be reached if index[0]
1887 # is `NaT`, in which case the call to `cls._generate_range` will
1888 # raise a ValueError, which we re-raise with a more targeted
1889 # message.
1890 raise ValueError(
1891 f"Inferred frequency {inferred} from passed values "
1892 f"does not conform to passed frequency {freq.freqstr}"
1893 ) from err
1894
1895 @classmethod
1896 def _generate_range(
1897 cls, start, end, periods: int | None, freq, *args, **kwargs
1898 ) -> Self:
1899 raise AbstractMethodError(cls)
1900
1901 # --------------------------------------------------------------
1902
1903 @cache_readonly
1904 def _creso(self) -> int:
1905 return get_unit_from_dtype(self._ndarray.dtype)
1906
1907 @cache_readonly
1908 def unit(self) -> TimeUnit:
1909 """
1910 The precision unit of the datetime data.
1911
1912 Returns the precision unit for the dtype.
1913 It means the smallest time frame that can be stored within this dtype.
1914
1915 Returns
1916 -------
1917 str
1918 Unit string representation (e.g. "ns").
1919
1920 See Also
1921 --------
1922 TimelikeOps.as_unit : Converts to a specific unit.
1923
1924 Examples
1925 --------
1926 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"])
1927 >>> idx.unit
1928 'ns'
1929 >>> idx.as_unit("s").unit
1930 's'
1931 """
1932 # error: Incompatible return value type (got "str", expected
1933 # "Literal['s', 'ms', 'us', 'ns']") [return-value]
1934 return dtype_to_unit(self.dtype) # type: ignore[return-value,arg-type]
1935
1936 def as_unit(self, unit: TimeUnit, round_ok: bool = True) -> Self:
1937 """
1938 Convert to a dtype with the given unit resolution.
1939
1940 The limits of timestamp representation depend on the chosen resolution.
1941 Different resolutions can be converted to each other through as_unit.
1942
1943 Parameters
1944 ----------
1945 unit : {'s', 'ms', 'us', 'ns'}
1946 round_ok : bool, default True
1947 If False and the conversion requires rounding, raise ValueError.
1948
1949 Returns
1950 -------
1951 same type as self
1952 Converted to the specified unit.
1953
1954 See Also
1955 --------
1956 Timestamp.as_unit : Convert to the given unit.
1957
1958 Examples
1959 --------
1960 For :class:`pandas.DatetimeIndex`:
1961
1962 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"])
1963 >>> idx
1964 DatetimeIndex(['2020-01-02 01:02:03.004005006'],
1965 dtype='datetime64[ns]', freq=None)
1966 >>> idx.as_unit("s")
1967 DatetimeIndex(['2020-01-02 01:02:03'], dtype='datetime64[s]', freq=None)
1968
1969 For :class:`pandas.TimedeltaIndex`:
1970
1971 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"])
1972 >>> tdelta_idx
1973 TimedeltaIndex(['1 days 00:03:00.000002042'],
1974 dtype='timedelta64[ns]', freq=None)
1975 >>> tdelta_idx.as_unit("s")
1976 TimedeltaIndex(['1 days 00:03:00'], dtype='timedelta64[s]', freq=None)
1977 """
1978 if unit not in ["s", "ms", "us", "ns"]:
1979 raise ValueError("Supported units are 's', 'ms', 'us', 'ns'")
1980
1981 dtype = np.dtype(f"{self.dtype.kind}8[{unit}]")
1982 new_values = astype_overflowsafe(self._ndarray, dtype, round_ok=round_ok)
1983
1984 if isinstance(self.dtype, np.dtype):
1985 new_dtype = new_values.dtype
1986 else:
1987 tz = cast("DatetimeArray", self).tz
1988 new_dtype = DatetimeTZDtype(tz=tz, unit=unit)
1989
1990 return type(self)._simple_new(
1991 new_values,
1992 dtype=new_dtype,
1993 )
1994
1995 # TODO: annotate other as DatetimeArray | TimedeltaArray | Timestamp | Timedelta
1996 # with the return type matching input type. TypeVar?
1997 def _ensure_matching_resos(self, other):
1998 if self._creso != other._creso:
1999 # Just as with Timestamp/Timedelta, we cast to the higher resolution
2000 if self._creso < other._creso:
2001 self = self.as_unit(other.unit)
2002 else:
2003 other = other.as_unit(self.unit)
2004 return self, other
2005
2006 # --------------------------------------------------------------
2007
2008 def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
2009 if (
2010 ufunc in [np.isnan, np.isinf, np.isfinite]
2011 and len(inputs) == 1
2012 and inputs[0] is self
2013 ):
2014 # numpy 1.18 changed isinf and isnan to not raise on dt64/td64
2015 return getattr(ufunc, method)(self._ndarray, **kwargs)
2016
2017 return super().__array_ufunc__(ufunc, method, *inputs, **kwargs)
2018
2019 def _round(self, freq, mode, ambiguous, nonexistent):
2020 # round the local times
2021 if isinstance(self.dtype, DatetimeTZDtype):
2022 # operate on naive timestamps, then convert back to aware
2023 self = cast("DatetimeArray", self)
2024 naive = self.tz_localize(None)
2025 result = naive._round(freq, mode, ambiguous, nonexistent)
2026 return result.tz_localize(
2027 self.tz, ambiguous=ambiguous, nonexistent=nonexistent
2028 )
2029
2030 values = self.view("i8")
2031 values = cast("np.ndarray", values)
2032 nanos = get_unit_for_round(freq, self._creso)
2033 if nanos == 0:
2034 # GH 52761
2035 return self.copy()
2036 result_i8 = round_nsint64(values, mode, nanos)
2037 result = self._maybe_mask_results(result_i8, fill_value=iNaT)
2038 result = result.view(self._ndarray.dtype)
2039 return self._simple_new(result, dtype=self.dtype)
2040
2041 def round( # type: ignore[override]
2042 self,
2043 freq,
2044 ambiguous: TimeAmbiguous = "raise",
2045 nonexistent: TimeNonexistent = "raise",
2046 ) -> Self:
2047 """
2048 Perform round operation on the data to the specified `freq`.
2049
2050 This method rounds each datetime value in the Series/Index to the
2051 nearest specified frequency using standard rounding rules (round half
2052 to even).
2053
2054 Parameters
2055 ----------
2056 freq : str or Offset
2057 The frequency level to round the index to. Must be a fixed
2058 frequency like 's' (second) not 'ME' (month end). See
2059 :ref:`frequency aliases <timeseries.offset_aliases>` for
2060 a list of possible `freq` values.
2061 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
2062 Only relevant for DatetimeIndex:
2063
2064 - 'infer' will attempt to infer fall dst-transition hours based on
2065 order. Requires that the timestamps are monotonically increasing.
2066 - bool-ndarray where True signifies a DST time, False designates
2067 a non-DST time (note that this flag is only applicable for
2068 ambiguous times)
2069 - 'NaT' will return NaT where there are ambiguous times
2070 - 'raise' will raise a ValueError if there are ambiguous
2071 times.
2072
2073 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \
2074 default 'raise'
2075 A nonexistent time does not exist in a particular timezone
2076 where clocks moved forward due to DST.
2077
2078 - 'shift_forward' will shift the nonexistent time forward to the
2079 closest existing time
2080 - 'shift_backward' will shift the nonexistent time backward to the
2081 closest existing time
2082 - 'NaT' will return NaT where there are nonexistent times
2083 - timedelta objects will shift nonexistent times by the timedelta
2084 - 'raise' will raise a ValueError if there are
2085 nonexistent times.
2086
2087 Returns
2088 -------
2089 DatetimeIndex, TimedeltaIndex, or Series
2090 Index of the same type for a DatetimeIndex or TimedeltaIndex,
2091 or a Series with the same index for a Series.
2092
2093 Raises
2094 ------
2095 ValueError if the `freq` cannot be converted.
2096
2097 See Also
2098 --------
2099 DatetimeIndex.floor :
2100 Perform floor operation on the data to the specified `freq`.
2101 DatetimeIndex.snap :
2102 Snap time stamps to nearest occurring frequency.
2103
2104 Notes
2105 -----
2106 If the timestamps have a timezone, rounding will take place relative to the
2107 local ("wall") time and re-localized to the same timezone. When rounding
2108 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to
2109 control the re-localization behavior.
2110
2111 Examples
2112 --------
2113 **DatetimeIndex**
2114
2115 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min")
2116 >>> rng
2117 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00',
2118 '2018-01-01 12:01:00'],
2119 dtype='datetime64[us]', freq='min')
2120
2121 >>> rng.round('h')
2122 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
2123 '2018-01-01 12:00:00'],
2124 dtype='datetime64[us]', freq=None)
2125
2126 **Series**
2127
2128 >>> pd.Series(rng).dt.round("h")
2129 0 2018-01-01 12:00:00
2130 1 2018-01-01 12:00:00
2131 2 2018-01-01 12:00:00
2132 dtype: datetime64[us]
2133
2134 When rounding near a daylight savings time transition, use ``ambiguous`` or
2135 ``nonexistent`` to control how the timestamp should be re-localized.
2136
2137 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 03:30:00"], tz="Europe/Amsterdam")
2138
2139 >>> rng_tz.floor("2h", ambiguous=False)
2140 DatetimeIndex(['2021-10-31 02:00:00+01:00'],
2141 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2142
2143 >>> rng_tz.floor("2h", ambiguous=True)
2144 DatetimeIndex(['2021-10-31 02:00:00+02:00'],
2145 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2146 """
2147 return self._round(freq, RoundTo.NEAREST_HALF_EVEN, ambiguous, nonexistent)
2148
2149 def floor(
2150 self,
2151 freq,
2152 ambiguous: TimeAmbiguous = "raise",
2153 nonexistent: TimeNonexistent = "raise",
2154 ) -> Self:
2155 """
2156 Perform floor operation on the data to the specified `freq`.
2157
2158 This method rounds each datetime value in the Series/Index down to
2159 the specified frequency (i.e., towards negative infinity).
2160
2161 Parameters
2162 ----------
2163 freq : str or Offset
2164 The frequency level to floor the index to. Must be a fixed
2165 frequency like 's' (second) not 'ME' (month end). See
2166 :ref:`frequency aliases <timeseries.offset_aliases>` for
2167 a list of possible `freq` values.
2168 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
2169 Only relevant for DatetimeIndex:
2170
2171 - 'infer' will attempt to infer fall dst-transition hours based on
2172 order. Requires that the timestamps are monotonically increasing.
2173 - bool-ndarray where True signifies a DST time, False designates
2174 a non-DST time (note that this flag is only applicable for
2175 ambiguous times)
2176 - 'NaT' will return NaT where there are ambiguous times
2177 - 'raise' will raise a ValueError if there are ambiguous
2178 times.
2179
2180 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \
2181 default 'raise'
2182 A nonexistent time does not exist in a particular timezone
2183 where clocks moved forward due to DST.
2184
2185 - 'shift_forward' will shift the nonexistent time forward to the
2186 closest existing time
2187 - 'shift_backward' will shift the nonexistent time backward to the
2188 closest existing time
2189 - 'NaT' will return NaT where there are nonexistent times
2190 - timedelta objects will shift nonexistent times by the timedelta
2191 - 'raise' will raise a ValueError if there are
2192 nonexistent times.
2193
2194 Returns
2195 -------
2196 DatetimeIndex, TimedeltaIndex, or Series
2197 Index of the same type for a DatetimeIndex or TimedeltaIndex,
2198 or a Series with the same index for a Series.
2199
2200 Raises
2201 ------
2202 ValueError if the `freq` cannot be converted.
2203
2204 See Also
2205 --------
2206 DatetimeIndex.floor :
2207 Perform floor operation on the data to the specified `freq`.
2208 DatetimeIndex.snap :
2209 Snap time stamps to nearest occurring frequency.
2210
2211 Notes
2212 -----
2213 If the timestamps have a timezone, flooring will take place relative to the
2214 local ("wall") time and re-localized to the same timezone. When flooring
2215 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to
2216 control the re-localization behavior.
2217
2218 Examples
2219 --------
2220 **DatetimeIndex**
2221
2222 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min")
2223 >>> rng
2224 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00',
2225 '2018-01-01 12:01:00'],
2226 dtype='datetime64[us]', freq='min')
2227
2228 >>> rng.floor('h')
2229 DatetimeIndex(['2018-01-01 11:00:00', '2018-01-01 12:00:00',
2230 '2018-01-01 12:00:00'],
2231 dtype='datetime64[us]', freq=None)
2232
2233 **Series**
2234
2235 >>> pd.Series(rng).dt.floor("h")
2236 0 2018-01-01 11:00:00
2237 1 2018-01-01 12:00:00
2238 2 2018-01-01 12:00:00
2239 dtype: datetime64[us]
2240
2241 When rounding near a daylight savings time transition, use ``ambiguous`` or
2242 ``nonexistent`` to control how the timestamp should be re-localized.
2243
2244 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 03:30:00"], tz="Europe/Amsterdam")
2245
2246 >>> rng_tz.floor("2h", ambiguous=False)
2247 DatetimeIndex(['2021-10-31 02:00:00+01:00'],
2248 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2249
2250 >>> rng_tz.floor("2h", ambiguous=True)
2251 DatetimeIndex(['2021-10-31 02:00:00+02:00'],
2252 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2253 """
2254 return self._round(freq, RoundTo.MINUS_INFTY, ambiguous, nonexistent)
2255
2256 def ceil(
2257 self,
2258 freq,
2259 ambiguous: TimeAmbiguous = "raise",
2260 nonexistent: TimeNonexistent = "raise",
2261 ) -> Self:
2262 """
2263 Perform ceil operation on the data to the specified `freq`.
2264
2265 This method rounds each datetime value in the Series/Index up to
2266 the specified frequency (i.e., towards positive infinity).
2267
2268 Parameters
2269 ----------
2270 freq : str or Offset
2271 The frequency level to ceil the index to. Must be a fixed
2272 frequency like 's' (second) not 'ME' (month end). See
2273 :ref:`frequency aliases <timeseries.offset_aliases>` for
2274 a list of possible `freq` values.
2275 ambiguous : 'infer', bool-ndarray, 'NaT', default 'raise'
2276 Only relevant for DatetimeIndex:
2277
2278 - 'infer' will attempt to infer fall dst-transition hours based on
2279 order. Requires that the timestamps are monotonically increasing.
2280 - bool-ndarray where True signifies a DST time, False designates
2281 a non-DST time (note that this flag is only applicable for
2282 ambiguous times)
2283 - 'NaT' will return NaT where there are ambiguous times
2284 - 'raise' will raise a ValueError if there are ambiguous
2285 times.
2286
2287 nonexistent : 'shift_forward', 'shift_backward', 'NaT', timedelta, \
2288 default 'raise'
2289 A nonexistent time does not exist in a particular timezone
2290 where clocks moved forward due to DST.
2291
2292 - 'shift_forward' will shift the nonexistent time forward to the
2293 closest existing time
2294 - 'shift_backward' will shift the nonexistent time backward to the
2295 closest existing time
2296 - 'NaT' will return NaT where there are nonexistent times
2297 - timedelta objects will shift nonexistent times by the timedelta
2298 - 'raise' will raise a ValueError if there are
2299 nonexistent times.
2300
2301 Returns
2302 -------
2303 DatetimeIndex, TimedeltaIndex, or Series
2304 Index of the same type for a DatetimeIndex or TimedeltaIndex,
2305 or a Series with the same index for a Series.
2306
2307 Raises
2308 ------
2309 ValueError if the `freq` cannot be converted.
2310
2311 See Also
2312 --------
2313 DatetimeIndex.floor :
2314 Perform floor operation on the data to the specified `freq`.
2315 DatetimeIndex.snap :
2316 Snap time stamps to nearest occurring frequency.
2317
2318 Notes
2319 -----
2320 If the timestamps have a timezone, ceiling will take place relative to the
2321 local ("wall") time and re-localized to the same timezone. When ceiling
2322 near daylight savings time, use ``nonexistent`` and ``ambiguous`` to
2323 control the re-localization behavior.
2324
2325 Examples
2326 --------
2327 **DatetimeIndex**
2328
2329 >>> rng = pd.date_range("1/1/2018 11:59:00", periods=3, freq="min")
2330 >>> rng
2331 DatetimeIndex(['2018-01-01 11:59:00', '2018-01-01 12:00:00',
2332 '2018-01-01 12:01:00'],
2333 dtype='datetime64[us]', freq='min')
2334
2335 >>> rng.ceil('h')
2336 DatetimeIndex(['2018-01-01 12:00:00', '2018-01-01 12:00:00',
2337 '2018-01-01 13:00:00'],
2338 dtype='datetime64[us]', freq=None)
2339
2340 **Series**
2341
2342 >>> pd.Series(rng).dt.ceil("h")
2343 0 2018-01-01 12:00:00
2344 1 2018-01-01 12:00:00
2345 2 2018-01-01 13:00:00
2346 dtype: datetime64[us]
2347
2348 When rounding near a daylight savings time transition, use ``ambiguous`` or
2349 ``nonexistent`` to control how the timestamp should be re-localized.
2350
2351 >>> rng_tz = pd.DatetimeIndex(["2021-10-31 01:30:00"], tz="Europe/Amsterdam")
2352
2353 >>> rng_tz.ceil("h", ambiguous=False)
2354 DatetimeIndex(['2021-10-31 02:00:00+01:00'],
2355 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2356
2357 >>> rng_tz.ceil("h", ambiguous=True)
2358 DatetimeIndex(['2021-10-31 02:00:00+02:00'],
2359 dtype='datetime64[us, Europe/Amsterdam]', freq=None)
2360 """
2361 return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent)
2362
2363 # --------------------------------------------------------------
2364 # Reductions
2365
2366 def any(self, *, axis: AxisInt | None = None, skipna: bool = True) -> bool:
2367 # GH#34479 the nanops call will raise a TypeError for non-td64 dtype
2368 return nanops.nanany(self._ndarray, axis=axis, skipna=skipna, mask=self.isna())
2369
2370 def all(self, *, axis: AxisInt | None = None, skipna: bool = True) -> bool:
2371 # GH#34479 the nanops call will raise a TypeError for non-td64 dtype
2372
2373 return nanops.nanall(self._ndarray, axis=axis, skipna=skipna, mask=self.isna())
2374
2375 # --------------------------------------------------------------
2376 # ExtensionArray Interface
2377
2378 def _values_for_json(self) -> np.ndarray:
2379 # Small performance bump vs the base class which calls np.asarray(self)
2380 if isinstance(self.dtype, np.dtype):
2381 return self._ndarray
2382 return super()._values_for_json()
2383
2384 def factorize(
2385 self,
2386 use_na_sentinel: bool = True,
2387 sort: bool = False,
2388 ):
2389 if sort:
2390 raise NotImplementedError(
2391 f"The 'sort' keyword in {type(self).__name__}.factorize is not "
2392 "supported. To factorize with sort, call pd.factorize(obj, sort=True) "
2393 "instead."
2394 )
2395 return super().factorize(use_na_sentinel=use_na_sentinel)
2396
2397 def interpolate(
2398 self,
2399 *,
2400 method: InterpolateOptions,
2401 axis: int,
2402 index: Index,
2403 limit,
2404 limit_direction,
2405 limit_area,
2406 copy: bool,
2407 **kwargs,
2408 ) -> Self:
2409 """
2410 See NDFrame.interpolate.__doc__.
2411 """
2412 # NB: we return type(self) even if copy=False
2413 if method != "linear":
2414 raise NotImplementedError
2415
2416 if not copy:
2417 out_data = self._ndarray
2418 else:
2419 out_data = self._ndarray.copy()
2420
2421 missing.interpolate_2d_inplace(
2422 out_data,
2423 method=method,
2424 axis=axis,
2425 index=index,
2426 limit=limit,
2427 limit_direction=limit_direction,
2428 limit_area=limit_area,
2429 **kwargs,
2430 )
2431 if not copy:
2432 return self
2433 return type(self)._simple_new(out_data, dtype=self.dtype)
2434
2435 # --------------------------------------------------------------
2436 # Unsorted
2437
2438 @property
2439 def _is_dates_only(self) -> bool:
2440 """
2441 Check if we are round times at midnight (and no timezone), which will
2442 be given a more compact __repr__ than other cases. For TimedeltaArray
2443 we are checking for multiples of 24H.
2444 """
2445 if not lib.is_np_dtype(self.dtype):
2446 # i.e. we have a timezone
2447 return False
2448
2449 values_int = self.asi8
2450 consider_values = values_int != iNaT
2451 reso = get_unit_from_dtype(self.dtype)
2452 ppd = periods_per_day(reso)
2453
2454 # TODO: can we reuse is_date_array_normalized? would need a skipna kwd
2455 # (first attempt at this was less performant than this implementation)
2456 even_days = np.logical_and(consider_values, values_int % ppd != 0).sum() == 0
2457 return even_days
2458
2459
2460# -------------------------------------------------------------------
2461# Shared Constructor Helpers
2462
2463
2464def ensure_arraylike_for_datetimelike(
2465 data, copy: bool, cls_name: str
2466) -> tuple[ArrayLike, bool]:
2467 if not hasattr(data, "dtype"):
2468 # e.g. list, tuple
2469 if not isinstance(data, (list, tuple)) and np.ndim(data) == 0:
2470 # i.e. generator
2471 data = list(data)
2472
2473 data = construct_1d_object_array_from_listlike(data)
2474 copy = False
2475 elif isinstance(data, ABCMultiIndex):
2476 raise TypeError(f"Cannot create a {cls_name} from a MultiIndex.")
2477 else:
2478 data = extract_array(data, extract_numpy=True)
2479
2480 if isinstance(data, IntegerArray) or (
2481 isinstance(data, ArrowExtensionArray) and data.dtype.kind in "iu"
2482 ):
2483 data = data.to_numpy("int64", na_value=iNaT)
2484 copy = False
2485 elif isinstance(data, ArrowExtensionArray):
2486 data = data._maybe_convert_datelike_array()
2487 data = data.to_numpy()
2488 copy = False
2489 elif not isinstance(data, (np.ndarray, ExtensionArray)):
2490 # GH#24539 e.g. xarray, dask object
2491 data = np.asarray(data)
2492
2493 elif isinstance(data, ABCCategorical):
2494 # GH#18664 preserve tz in going DTI->Categorical->DTI
2495 # TODO: cases where we need to do another pass through maybe_convert_dtype,
2496 # e.g. the categories are timedelta64s
2497 data = data.categories.take(data.codes, allow_fill=True, fill_value=NaT)._values
2498 copy = False
2499
2500 if data.ndim == 0:
2501 raise ValueError(f"Cannot construct {cls_name} from 0-dim input")
2502
2503 return data, copy
2504
2505
2506@overload
2507def validate_periods(periods: None) -> None: ...
2508
2509
2510@overload
2511def validate_periods(periods: int) -> int: ...
2512
2513
2514def validate_periods(periods: int | None) -> int | None:
2515 """
2516 If a `periods` argument is passed to the Datetime/Timedelta Array/Index
2517 constructor, cast it to an integer.
2518
2519 Parameters
2520 ----------
2521 periods : None, int
2522
2523 Returns
2524 -------
2525 periods : None or int
2526
2527 Raises
2528 ------
2529 TypeError
2530 if periods is not None or int
2531 """
2532 if periods is not None and not lib.is_integer(periods):
2533 raise TypeError(f"periods must be an integer, got {periods}")
2534 # error: Incompatible return value type (got "int | integer[Any] | None",
2535 # expected "int | None")
2536 return periods # type: ignore[return-value]
2537
2538
2539def dtype_to_unit(dtype: DatetimeTZDtype | np.dtype | ArrowDtype) -> str:
2540 """
2541 Return the unit str corresponding to the dtype's resolution.
2542
2543 Parameters
2544 ----------
2545 dtype : DatetimeTZDtype or np.dtype
2546 If np.dtype, we assume it is a datetime64 dtype.
2547
2548 Returns
2549 -------
2550 str
2551 """
2552 if isinstance(dtype, DatetimeTZDtype):
2553 return dtype.unit
2554 elif isinstance(dtype, ArrowDtype):
2555 if dtype.kind not in "mM":
2556 raise ValueError(f"{dtype=} does not have a resolution.")
2557 return dtype.pyarrow_dtype.unit
2558 return np.datetime_data(dtype)[0]