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]