1"""
2Base and utility classes for tseries type pandas objects.
3"""
4
5from __future__ import annotations
6
7from abc import (
8 ABC,
9)
10from itertools import pairwise
11import operator
12from typing import (
13 TYPE_CHECKING,
14 Any,
15 Literal,
16 Self,
17 cast,
18 final,
19)
20import warnings
21
22import numpy as np
23
24from pandas._libs import (
25 NaT,
26 lib,
27)
28from pandas._libs.tslibs import (
29 BaseOffset,
30 Day,
31 Resolution,
32 Tick,
33 Timedelta,
34 Timestamp,
35 get_resolution,
36 parsing,
37 timezones,
38 to_offset,
39)
40from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
41from pandas._libs.tslibs.offsets import (
42 RelativeDeltaOffset,
43 Week,
44)
45from pandas.compat.numpy import function as nv
46from pandas.errors import (
47 InvalidIndexError,
48 NullFrequencyError,
49 OutOfBoundsDatetime,
50 OutOfBoundsTimedelta,
51 Pandas4Warning,
52)
53from pandas.util._decorators import (
54 cache_readonly,
55)
56from pandas.util._exceptions import find_stack_level
57
58from pandas.core.dtypes.common import (
59 is_integer,
60 is_list_like,
61)
62from pandas.core.dtypes.concat import concat_compat
63from pandas.core.dtypes.dtypes import (
64 CategoricalDtype,
65 DatetimeTZDtype,
66 PeriodDtype,
67)
68
69from pandas.core import (
70 algorithms,
71 roperator,
72)
73from pandas.core.arrays import (
74 DatetimeArray,
75 ExtensionArray,
76 PeriodArray,
77 TimedeltaArray,
78)
79import pandas.core.common as com
80from pandas.core.construction import ensure_wrapped_if_datetimelike
81from pandas.core.indexers import check_array_indexer
82from pandas.core.indexes.base import (
83 Index,
84)
85from pandas.core.indexes.extension import NDArrayBackedExtensionIndex
86from pandas.core.indexes.range import RangeIndex
87from pandas.core.tools.timedeltas import to_timedelta
88
89from pandas.tseries import frequencies
90
91if TYPE_CHECKING:
92 from collections.abc import (
93 Hashable,
94 Sequence,
95 )
96 from datetime import datetime
97
98 from pandas._typing import (
99 Axis,
100 JoinHow,
101 TimeUnit,
102 npt,
103 )
104
105 from pandas import CategoricalIndex
106
107
108class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC):
109 """
110 Common ops mixin to support a unified interface datetimelike Index.
111 """
112
113 _can_hold_strings = False
114 _data: DatetimeArray | TimedeltaArray | PeriodArray
115 _warn_quarter: bool = True
116
117 def mean(self, *, skipna: bool = True, axis: int | None = 0):
118 """
119 Return the mean value of the Array.
120
121 This method computes the arithmetic mean of the datetime or timedelta
122 values in the index, optionally skipping NaT values.
123
124 Parameters
125 ----------
126 skipna : bool, default True
127 Whether to ignore any NaT elements.
128 axis : int, optional, default 0
129 Axis for the function to be applied on.
130
131 Returns
132 -------
133 scalar
134 Timestamp or Timedelta.
135
136 See Also
137 --------
138 numpy.ndarray.mean : Returns the average of array elements along a given axis.
139 Series.mean : Return the mean value in a Series.
140
141 Notes
142 -----
143 mean is only defined for Datetime and Timedelta dtypes, not for Period.
144
145 Examples
146 --------
147 For :class:`pandas.DatetimeIndex`:
148
149 >>> idx = pd.date_range("2001-01-01 00:00", periods=3)
150 >>> idx
151 DatetimeIndex(['2001-01-01', '2001-01-02', '2001-01-03'],
152 dtype='datetime64[us]', freq='D')
153 >>> idx.mean()
154 Timestamp('2001-01-02 00:00:00')
155
156 For :class:`pandas.TimedeltaIndex`:
157
158 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
159 >>> tdelta_idx
160 TimedeltaIndex(['1 days', '2 days', '3 days'],
161 dtype='timedelta64[us]', freq=None)
162 >>> tdelta_idx.mean()
163 Timedelta('2 days 00:00:00')
164 """
165 return self._data.mean(skipna=skipna, axis=axis)
166
167 @property
168 def freq(self) -> BaseOffset | None:
169 """
170 Return the frequency object if it is set, otherwise None.
171
172 To learn more about the frequency strings, please see
173 :ref:`this link<timeseries.offset_aliases>`.
174
175 See Also
176 --------
177 DatetimeIndex.freq : Return the frequency object if it is set, otherwise None.
178 PeriodIndex.freq : Return the frequency object if it is set, otherwise None.
179
180 Examples
181 --------
182 >>> datetimeindex = pd.date_range(
183 ... "2022-02-22 02:22:22", periods=10, tz="America/Chicago", freq="h"
184 ... )
185 >>> datetimeindex
186 DatetimeIndex(['2022-02-22 02:22:22-06:00', '2022-02-22 03:22:22-06:00',
187 '2022-02-22 04:22:22-06:00', '2022-02-22 05:22:22-06:00',
188 '2022-02-22 06:22:22-06:00', '2022-02-22 07:22:22-06:00',
189 '2022-02-22 08:22:22-06:00', '2022-02-22 09:22:22-06:00',
190 '2022-02-22 10:22:22-06:00', '2022-02-22 11:22:22-06:00'],
191 dtype='datetime64[us, America/Chicago]', freq='h')
192 >>> datetimeindex.freq
193 <Hour>
194 """
195 # PeriodIndex reads from the array (derived from dtype);
196 # DatetimeTimedeltaMixin overrides to read from self._freq.
197 return self._data.freq # type: ignore[union-attr]
198
199 @property
200 def asi8(self) -> npt.NDArray[np.int64]:
201 """
202 Return Integer representation of the values.
203
204 For :class:`DatetimeIndex` and :class:`TimedeltaIndex`, the
205 values are the number of time units (determined by the index
206 resolution) since the epoch. For :class:`PeriodIndex`, the
207 values are ordinals.
208
209 Returns
210 -------
211 numpy.ndarray
212 An ndarray with int64 dtype.
213
214 See Also
215 --------
216 Index.values : Return an array representing the data in the Index,
217 using native types (datetime64, timedelta64) rather than int64.
218 Index.to_numpy : Return a NumPy ndarray of the index values.
219
220 Examples
221 --------
222 For :class:`DatetimeIndex` with default microsecond resolution:
223
224 >>> idx = pd.DatetimeIndex(["2023-01-01", "2023-01-02"], dtype="datetime64[us]")
225 >>> idx.asi8
226 array([1672531200000000, 1672617600000000])
227
228 For :class:`TimedeltaIndex` with millisecond resolution:
229
230 >>> idx = pd.TimedeltaIndex(["1 day", "2 days"], dtype="timedelta64[ms]")
231 >>> idx.asi8
232 array([ 86400000, 172800000])
233
234 For :class:`PeriodIndex`:
235
236 >>> idx = pd.PeriodIndex(["2023-01", "2023-02", "2023-03"], freq="M")
237 >>> idx.asi8
238 array([636, 637, 638])
239 """
240 return self._data.asi8
241
242 @property
243 def freqstr(self) -> str:
244 """
245 Return the frequency object as a string if it's set, otherwise None.
246
247 This property returns a string representation of the frequency
248 (e.g., ``'D'`` for daily, ``'h'`` for hourly) when one has been set
249 on the index, either explicitly or via inference.
250
251 See Also
252 --------
253 DatetimeIndex.inferred_freq : Returns a string representing a frequency
254 generated by infer_freq.
255
256 Examples
257 --------
258 For DatetimeIndex:
259
260 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00"], freq="D")
261 >>> idx.freqstr
262 'D'
263
264 The frequency can be inferred if there are more than 2 points:
265
266 >>> idx = pd.DatetimeIndex(
267 ... ["2018-01-01", "2018-01-03", "2018-01-05"], freq="infer"
268 ... )
269 >>> idx.freqstr
270 '2D'
271
272 For PeriodIndex:
273
274 >>> idx = pd.PeriodIndex(["2023-1", "2023-2", "2023-3"], freq="M")
275 >>> idx.freqstr
276 'M'
277 """
278 freq = self.freq
279 if freq is None:
280 return freq # type: ignore[return-value]
281 if isinstance(self._data, PeriodArray):
282 return PeriodDtype(freq)._freqstr
283 return freq.freqstr
284
285 @cache_readonly
286 def _resolution_obj(self) -> Resolution:
287 if isinstance(self.dtype, PeriodDtype):
288 return self.dtype._resolution_obj
289 elif self.dtype.kind == "M":
290 return get_resolution(self.asi8, self.tz, reso=self._data._creso) # type: ignore[attr-defined,union-attr]
291 else:
292 return get_resolution(self.asi8, tz=None, reso=self._data._creso) # type: ignore[union-attr]
293
294 @cache_readonly
295 def resolution(self) -> str:
296 """
297 Returns day, hour, minute, second, millisecond or microsecond
298 """
299 return self._resolution_obj.attrname
300
301 # ------------------------------------------------------------------------
302
303 @cache_readonly
304 def hasnans(self) -> bool:
305 return self._data._hasna
306
307 def equals(self, other: Any) -> bool:
308 """
309 Determines if two Index objects contain the same elements.
310 """
311 if self.is_(other):
312 return True
313
314 if not isinstance(other, Index):
315 return False
316 elif other.dtype.kind in "iufc":
317 return False
318 elif not isinstance(other, type(self)):
319 if other.dtype == object:
320 # Use the cached inferred_type as a cheap pre-filter
321 inferred = other.inferred_type
322 compatible: tuple[str, ...]
323 if self.dtype.kind == "M":
324 compatible = ("datetime", "datetime64", "date")
325 elif self.dtype.kind == "m":
326 compatible = ("timedelta", "timedelta64")
327 else:
328 compatible = ("period",)
329 if inferred not in compatible:
330 return False
331 converted = lib.maybe_convert_objects(
332 np.asarray(other), convert_non_numeric=True
333 )
334 converted = ensure_wrapped_if_datetimelike(converted)
335 if isinstance(converted, type(self._data)):
336 other = type(self)._simple_new(converted)
337 elif self.dtype.kind == "M" and inferred == "date":
338 # GH#65056
339 warnings.warn(
340 "Inferring datetime64 from data containing "
341 "datetime.date objects is deprecated. In a future "
342 "version, these will be left as object dtype. Use "
343 "pd.to_datetime to explicitly convert to datetime64.",
344 Pandas4Warning,
345 stacklevel=find_stack_level(),
346 )
347 try:
348 other = type(self)(other)
349 except (ValueError, TypeError, OverflowError):
350 return False
351 elif isinstance(other.dtype, CategoricalDtype):
352 other = cast("CategoricalIndex", other)
353 cat_vals = np.asarray(other.categories)
354 if cat_vals.dtype == object:
355 converted = lib.maybe_convert_objects(
356 cat_vals, convert_non_numeric=True
357 )
358 converted = ensure_wrapped_if_datetimelike(converted)
359 else:
360 converted = ensure_wrapped_if_datetimelike(cat_vals)
361 if isinstance(converted, type(self._data)):
362 try:
363 other = type(self)(other)
364 except (ValueError, TypeError, OverflowError):
365 return False
366 elif (
367 self.dtype.kind == "M"
368 and cat_vals.dtype == object
369 and lib.infer_dtype(other.categories) == "date"
370 ):
371 # GH#65056
372 warnings.warn(
373 "Inferring datetime64 from data containing "
374 "datetime.date objects is deprecated. In a "
375 "future version, these will be left as object "
376 "dtype. Use pd.to_datetime to explicitly "
377 "convert to datetime64.",
378 Pandas4Warning,
379 stacklevel=find_stack_level(),
380 )
381 try:
382 other = type(self)(other)
383 except (ValueError, TypeError, OverflowError):
384 return False
385
386 if type(self) != type(other):
387 return False
388 elif self.dtype == other.dtype:
389 return lib.array_equivalent_bytes(self.asi8, other.asi8)
390 elif (self.dtype.kind == "M" and self.tz == other.tz) or self.dtype.kind == "m": # type: ignore[attr-defined]
391 # different units, otherwise matching
392 try:
393 # TODO: do this at the EA level?
394 left, right = self._data._ensure_matching_resos(other._data) # type: ignore[union-attr]
395 except (OutOfBoundsDatetime, OutOfBoundsTimedelta):
396 return False
397 else:
398 return lib.array_equivalent_bytes(left.view("i8"), right.view("i8"))
399 return False
400
401 def __contains__(self, key: Any) -> bool:
402 """
403 Return a boolean indicating whether the provided key is in the index.
404
405 Parameters
406 ----------
407 key : label
408 The key to check if it is present in the index.
409
410 Returns
411 -------
412 bool
413 Whether the key search is in the index.
414
415 Raises
416 ------
417 TypeError
418 If the key is not hashable.
419
420 See Also
421 --------
422 Index.isin : Returns an ndarray of boolean dtype indicating whether the
423 list-like key is in the index.
424
425 Examples
426 --------
427 >>> idx = pd.Index([1, 2, 3, 4])
428 >>> idx
429 Index([1, 2, 3, 4], dtype='int64')
430 >>> 2 in idx
431 True
432 >>> 6 in idx
433 False
434 """
435 hash(key)
436 try:
437 self.get_loc(key)
438 except (KeyError, TypeError, ValueError, InvalidIndexError):
439 return False
440 return True
441
442 def _convert_tolerance(self, tolerance, target):
443 tolerance = np.asarray(to_timedelta(tolerance).to_numpy())
444 return super()._convert_tolerance(tolerance, target)
445
446 # --------------------------------------------------------------------
447 # Rendering Methods
448 _default_na_rep = "NaT"
449
450 def _format_with_header(
451 self, *, header: list[str], na_rep: str, date_format: str | None = None
452 ) -> list[str]:
453 # TODO: not reached in tests 2023-10-11
454 # matches base class except for whitespace padding and date_format
455 return header + list(
456 self._get_values_for_csv(na_rep=na_rep, date_format=date_format)
457 )
458
459 def _formatter_func(self, val) -> str:
460 return self._data._formatter()(val)
461
462 def _format_attrs(self):
463 """
464 Return a list of tuples of the (attr,formatted_value).
465 """
466 attrs = super()._format_attrs()
467 for attrib in self._attributes:
468 # iterating over _attributes prevents us from doing this for PeriodIndex
469 if attrib == "freq":
470 freq = self.freqstr
471 if freq is not None:
472 freq = repr(freq) # e.g. D -> 'D'
473 attrs.append(("freq", freq))
474 return attrs
475
476 def _summary(self, name=None) -> str:
477 """
478 Return a summarized representation.
479
480 Parameters
481 ----------
482 name : str
483 name to use in the summary representation
484
485 Returns
486 -------
487 String with a summarized representation of the index
488 """
489 result = super()._summary(name=name)
490 if self.freq:
491 result += f"\nFreq: {self.freqstr}"
492
493 return result
494
495 # --------------------------------------------------------------------
496 # Indexing Methods
497
498 @final
499 def _can_partial_date_slice(self, reso: Resolution) -> bool:
500 # e.g. test_getitem_setitem_periodindex
501 # History of conversation GH#3452, GH#3931, GH#2369, GH#14826
502 return reso > self._resolution_obj
503 # NB: for DTI/PI, not TDI
504
505 def _parsed_string_to_bounds(self, reso: Resolution, parsed):
506 raise NotImplementedError
507
508 def _parse_with_reso(self, label: str) -> tuple[datetime, Resolution]:
509 # overridden by TimedeltaIndex
510 freqstr: str | None
511 if self.freq is None:
512 freqstr = None
513 else:
514 try:
515 freqstr = self.freq.rule_code
516 except NotImplementedError:
517 # e.g. a generic DateOffset, which has no rule_code; it also
518 # carries no quarter/month anchor, so None is the right answer
519 freqstr = None
520
521 if isinstance(label, np.str_):
522 # GH#45580
523 label = str(label)
524
525 parsed, reso_str = parsing.parse_datetime_string_with_reso(
526 label,
527 freqstr,
528 warn_quarter=self._warn_quarter,
529 )
530 reso = Resolution.from_attrname(reso_str)
531 return parsed, reso
532
533 def _get_string_slice(self, key: str) -> slice | npt.NDArray[np.intp]: # type: ignore[override]
534 # overridden by TimedeltaIndex
535 parsed, reso = self._parse_with_reso(key)
536 try:
537 return self._partial_date_slice(reso, parsed)
538 except KeyError as err:
539 raise KeyError(key) from err
540
541 @final
542 def _partial_date_slice(
543 self,
544 reso: Resolution,
545 parsed: datetime,
546 ) -> slice | npt.NDArray[np.intp]:
547 """
548 Parameters
549 ----------
550 reso : Resolution
551 parsed : datetime
552
553 Returns
554 -------
555 slice or ndarray[intp]
556 """
557 if not self._can_partial_date_slice(reso):
558 raise ValueError
559
560 t1, t2 = self._parsed_string_to_bounds(reso, parsed)
561 vals = self._data._ndarray
562 unbox = self._data._unbox
563
564 if self.is_monotonic_increasing:
565 if len(self) and (
566 (t1 < self[0] and t2 < self[0]) or (t1 > self[-1] and t2 > self[-1])
567 ):
568 # we are out of range
569 raise KeyError
570
571 # a monotonic increasing series can be sliced
572 # (searchsorted requires ascending order)
573 left = vals.searchsorted(unbox(t1), side="left")
574 right = vals.searchsorted(unbox(t2), side="right")
575 return slice(left, right)
576
577 elif self.is_monotonic_decreasing:
578 if len(self) and (
579 (t1 > self[0] and t2 > self[0]) or (t1 < self[-1] and t2 < self[-1])
580 ):
581 # we are out of range
582 raise KeyError
583
584 # searchsorted requires ascending order, so search the reversed
585 # array and convert the indices back
586 reversed_vals = vals[::-1]
587 nvals = len(vals)
588 rev_left = reversed_vals.searchsorted(unbox(t1), side="left")
589 rev_right = reversed_vals.searchsorted(unbox(t2), side="right")
590 return slice(nvals - rev_right, nvals - rev_left)
591
592 else:
593 lhs_mask = vals >= unbox(t1)
594 rhs_mask = vals <= unbox(t2)
595
596 # try to find the dates
597 return (lhs_mask & rhs_mask).nonzero()[0]
598
599 def _maybe_cast_slice_bound(self, label, side: str):
600 """
601 If label is a string, cast it to scalar type according to resolution.
602
603 Parameters
604 ----------
605 label : object
606 side : {'left', 'right'}
607
608 Returns
609 -------
610 label : object
611
612 Notes
613 -----
614 Value of `side` parameter should be validated in caller.
615 """
616 if isinstance(label, str):
617 try:
618 parsed, reso = self._parse_with_reso(label)
619 except ValueError as err:
620 # DTI -> parsing.DateParseError
621 # TDI -> 'unit abbreviation w/o a number'
622 # PI -> string cannot be parsed as datetime-like
623 self._raise_invalid_indexer("slice", label, err)
624
625 lower, upper = self._parsed_string_to_bounds(reso, parsed)
626 return lower if side == "left" else upper
627 elif not isinstance(label, self._data._recognized_scalars):
628 self._raise_invalid_indexer("slice", label)
629
630 return label
631
632 # --------------------------------------------------------------------
633 # Arithmetic Methods
634
635 def shift(self, periods: int = 1, freq=None) -> Self:
636 """
637 Shift index by desired number of time frequency increments.
638
639 This method is for shifting the values of datetime-like indexes
640 by a specified time increment a given number of times.
641
642 Parameters
643 ----------
644 periods : int, default 1
645 Number of periods (or increments) to shift by,
646 can be positive or negative.
647 freq : pandas.DateOffset, pandas.Timedelta or string, optional
648 Frequency increment to shift by.
649 If None, the index is shifted by its own `freq` attribute.
650 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
651
652 Returns
653 -------
654 pandas.DatetimeIndex
655 Shifted index.
656
657 See Also
658 --------
659 Index.shift : Shift values of Index.
660 PeriodIndex.shift : Shift values of PeriodIndex.
661 """
662 raise NotImplementedError
663
664 # --------------------------------------------------------------------
665
666 def _maybe_cast_listlike_indexer(self, keyarr):
667 """
668 Analogue to maybe_cast_indexer for get_indexer instead of get_loc.
669 """
670 try:
671 res = self._data._validate_listlike(keyarr, allow_object=True)
672 except (ValueError, TypeError):
673 if not isinstance(keyarr, ExtensionArray):
674 # e.g. we don't want to cast DTA to ndarray[object]
675 res = com.asarray_tuplesafe(keyarr)
676 # TODO: com.asarray_tuplesafe shouldn't cast e.g. DatetimeArray
677 else:
678 res = keyarr
679 return Index(res, dtype=res.dtype)
680
681
682class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC):
683 """
684 Mixin class for methods shared by DatetimeIndex and TimedeltaIndex,
685 but not PeriodIndex
686 """
687
688 _data: DatetimeArray | TimedeltaArray
689 _comparables = ["name", "freq"]
690 _attributes = ["name", "freq"]
691 _freq: BaseOffset | None = None
692
693 # Compat for frequency inference, see GH#23789
694 _is_monotonic_increasing = Index.is_monotonic_increasing
695 _is_monotonic_decreasing = Index.is_monotonic_decreasing
696 _is_unique = Index.is_unique
697
698 @property
699 def freq(self) -> BaseOffset | None:
700 """
701 Return the frequency object if it is set, otherwise None.
702
703 To learn more about the frequency strings, please see
704 :ref:`this link<timeseries.offset_aliases>`.
705
706 See Also
707 --------
708 DatetimeIndex.freq : Return the frequency object if it is set, otherwise None.
709 PeriodIndex.freq : Return the frequency object if it is set, otherwise None.
710
711 Examples
712 --------
713 >>> datetimeindex = pd.date_range(
714 ... "2022-02-22 02:22:22", periods=10, tz="America/Chicago", freq="h"
715 ... )
716 >>> datetimeindex
717 DatetimeIndex(['2022-02-22 02:22:22-06:00', '2022-02-22 03:22:22-06:00',
718 '2022-02-22 04:22:22-06:00', '2022-02-22 05:22:22-06:00',
719 '2022-02-22 06:22:22-06:00', '2022-02-22 07:22:22-06:00',
720 '2022-02-22 08:22:22-06:00', '2022-02-22 09:22:22-06:00',
721 '2022-02-22 10:22:22-06:00', '2022-02-22 11:22:22-06:00'],
722 dtype='datetime64[us, America/Chicago]', freq='h')
723 >>> datetimeindex.freq
724 <Hour>
725 """
726 return self._freq
727
728 @freq.setter
729 def freq(self, value) -> None:
730 arr = self._data
731 if value is not None:
732 value = to_offset(value)
733 arr._validate_frequency(arr, value)
734 if arr.dtype.kind == "m" and not isinstance(value, (Tick, Day)):
735 raise TypeError("TimedeltaArray/Index freq must be a Tick")
736
737 if arr.ndim > 1:
738 raise ValueError("Cannot set freq with ndim > 1")
739
740 self._freq = value
741
742 def astype(self, dtype, copy: bool = True):
743 result = super().astype(dtype, copy=copy)
744 if isinstance(result, type(self)):
745 # Preserve freq for unit conversions (e.g. datetime64[ns] -> [us])
746 result._freq = self._freq
747 return result
748
749 def putmask(self, mask, value) -> Index:
750 # GH#24555 putmask may modify values out-of-sequence; drop freq
751 result = super().putmask(mask, value)
752 if isinstance(result, type(self)):
753 result._freq = None
754 return result
755
756 def _view(self) -> Self:
757 result = super()._view()
758 result._freq = self._freq
759 return result
760
761 def copy(self, name: Hashable | None = None, deep: bool = False) -> Self:
762 result = super().copy(name=name, deep=deep)
763 result._freq = self._freq
764 return result
765
766 def _pin_freq(self, freq, inferred) -> None:
767 """
768 Constructor helper to pin the appropriate ``freq`` attribute on self.
769
770 ``inferred`` is the frequency inferred from the input data (or ``None``
771 if no freq could be inferred), as determined by the caller before
772 wrapping the array in this Index.
773 """
774 arr = self._data
775 if freq is None:
776 # user explicitly passed None -> override any inferred_freq
777 self._freq = None
778 elif freq == "infer":
779 if inferred is not None:
780 # already inferred during construction
781 self._freq = inferred
782 else:
783 # Set _freq directly to bypass duplicative _validate_frequency
784 # check.
785 self._freq = to_offset(self._inferred_freq_str)
786 elif freq is lib.no_default:
787 # user did not specify anything, keep inferred freq if the original
788 # data had one, otherwise leave as None (class default)
789 self._freq = inferred
790 elif inferred is None:
791 # We cannot inherit a freq from the data, so we need to validate
792 # the user-passed freq
793 freq = to_offset(freq)
794 type(arr)._validate_frequency(self, freq)
795 self._freq = freq
796 else:
797 # Otherwise we just need to check that the user-passed freq
798 # doesn't conflict with the one we already have.
799 freq = to_offset(freq)
800 if freq != inferred:
801 # GH#61086 freq may be equivalent but not equal (e.g.
802 # QS-FEB vs QS-MAY), so validate against the actual data.
803 # GH#65012 checking a single step is not enough: a fixed-step
804 # (Tick/Day) or business/custom offset can match the first gap
805 # by coincidence yet diverge on a later step, so every step
806 # must conform.
807 if len(self) == 1:
808 if not freq.is_on_offset(self[0]):
809 raise ValueError(
810 f"Inferred frequency {inferred} from passed "
811 "values does not conform to passed frequency "
812 f"{freq.freqstr}"
813 )
814 elif isinstance(freq, Tick) and len(self) > 1:
815 # A Tick is a fixed step in i8 space regardless of tz, so
816 # conformance is just uniform spacing. This avoids the
817 # pricier _validate_frequency (which infers the freq and
818 # allocates a range). Not valid for Day (DST-dependent) or
819 # calendar offsets, which stay on the path below.
820 delta = Timedelta(freq)
821 step = delta.as_unit(self.unit)
822 if step != delta or not (np.diff(self.asi8) == step._value).all():
823 raise ValueError(
824 f"Inferred frequency {inferred} from passed "
825 "values does not conform to passed frequency "
826 f"{freq.freqstr}"
827 )
828 elif len(self) > 1:
829 type(arr)._validate_frequency(self, freq)
830 self._freq = freq
831
832 def _get_arithmetic_result_freq(self, other) -> BaseOffset | None:
833 """
834 Check if we can preserve self.freq in addition or subtraction.
835 """
836 if not lib.is_scalar(other):
837 return None
838
839 # Normalize scalar types to their pandas equivalents.
840 # Array arithmetic methods internally convert Tick/timedelta/
841 # np.timedelta64 → Timedelta and datetime/np.datetime64 → Timestamp.
842 # Since we receive the original user argument, normalize here.
843 if isinstance(other, Tick):
844 other = Timedelta(other)
845 elif isinstance(other, np.timedelta64):
846 other = Timedelta(other)
847 elif isinstance(other, np.datetime64):
848 other = Timestamp(other)
849 elif not isinstance(other, (Timedelta, Timestamp)):
850 # Handles datetime.timedelta, datetime.datetime
851 try:
852 other = Timedelta(other)
853 except (ValueError, TypeError, OverflowError):
854 try:
855 other = Timestamp(other)
856 except (ValueError, TypeError, OverflowError):
857 return None
858
859 if isinstance(self.freq, Tick):
860 return self.freq
861 elif self.dtype.kind == "m" and isinstance(other, Timedelta):
862 return self.freq
863 elif (
864 self.dtype.kind == "m"
865 and isinstance(other, Timestamp)
866 and (other.tz is None or timezones.is_utc(other.tz))
867 ):
868 return self.freq
869 elif (
870 lib.is_np_dtype(self.dtype, "M")
871 and isinstance(self.freq, Day)
872 and isinstance(other, (Timedelta, Timestamp))
873 ):
874 return self.freq
875
876 return None
877
878 def _arith_method(self, other, op):
879 result = super()._arith_method(other, op)
880 if result is NotImplemented:
881 return result
882 if op in (operator.add, roperator.radd, operator.sub, roperator.rsub):
883 new_freq = self._get_arithmetic_result_freq(other)
884 if new_freq is not None and isinstance(result, DatetimeTimedeltaMixin):
885 if op is roperator.rsub:
886 new_freq = -new_freq
887 result._freq = new_freq
888 return result
889
890 def factorize(
891 self,
892 sort: bool = False,
893 use_na_sentinel: bool = True,
894 ):
895 if self.freq is not None:
896 # We must be unique, so can short-circuit (and retain freq)
897 if sort and self.freq.n < 0:
898 codes = np.arange(len(self) - 1, -1, -1, dtype=np.intp)
899 uniques = self[::-1]
900 else:
901 codes = np.arange(len(self), dtype=np.intp)
902 uniques = self.copy()
903 uniques._name = None
904 return codes, uniques
905 if len(self) > 1 and (
906 self.is_monotonic_increasing or self.is_monotonic_decreasing
907 ):
908 # Monotonic implies no NaT, so NA handling is not needed
909 codes, uniques_indexer = algorithms.factorize_monotonic_codes(
910 self._data._ndarray, self.is_monotonic_increasing, sort
911 )
912 uniques = self[uniques_indexer]
913 uniques._name = None
914 return codes, uniques
915 return super().factorize(sort=sort, use_na_sentinel=use_na_sentinel)
916
917 @property
918 def unit(self) -> TimeUnit:
919 """
920 The precision unit of the datetime data.
921
922 Returns the precision unit for the dtype of the index. This is the
923 smallest time frame that can be stored within this dtype.
924
925 Returns
926 -------
927 str
928 Unit string representation (e.g. ``"ns"``).
929
930 See Also
931 --------
932 DatetimeIndex.as_unit : Convert to the given unit.
933 TimedeltaIndex.as_unit : Convert to the given unit.
934
935 Examples
936 --------
937 For a DatetimeIndex:
938
939 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"])
940 >>> idx.unit
941 'ns'
942
943 >>> idx_s = pd.DatetimeIndex(["2020-01-02 01:02:03"], dtype="datetime64[s]")
944 >>> idx_s.unit
945 's'
946
947 For a TimedeltaIndex:
948
949 >>> tdidx = pd.TimedeltaIndex(["1 day 3 min 2 us 42 ns"])
950 >>> tdidx.unit
951 'ns'
952
953 >>> tdidx_s = pd.TimedeltaIndex(["1 day 3 min"], dtype="timedelta64[s]")
954 >>> tdidx_s.unit
955 's'
956 """
957 return self._data.unit
958
959 def as_unit(self, unit: TimeUnit) -> Self:
960 """
961 Convert to a dtype with the given unit resolution.
962
963 This method is for converting the dtype of a ``DatetimeIndex`` or
964 ``TimedeltaIndex`` to a new dtype with the given unit
965 resolution/precision.
966
967 Parameters
968 ----------
969 unit : {'s', 'ms', 'us', 'ns'}
970
971 Returns
972 -------
973 same type as self
974 Converted to the specified unit.
975
976 See Also
977 --------
978 Timestamp.as_unit : Convert to the given unit.
979 Timedelta.as_unit : Convert to the given unit.
980 DatetimeIndex.as_unit : Convert to the given unit.
981 TimedeltaIndex.as_unit : Convert to the given unit.
982
983 Examples
984 --------
985 For :class:`pandas.DatetimeIndex`:
986
987 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"])
988 >>> idx
989 DatetimeIndex(['2020-01-02 01:02:03.004005006'],
990 dtype='datetime64[ns]', freq=None)
991 >>> idx.as_unit("s")
992 DatetimeIndex(['2020-01-02 01:02:03'], dtype='datetime64[s]', freq=None)
993
994 For :class:`pandas.TimedeltaIndex`:
995
996 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"])
997 >>> tdelta_idx
998 TimedeltaIndex(['1 days 00:03:00.000002042'],
999 dtype='timedelta64[ns]', freq=None)
1000 >>> tdelta_idx.as_unit("s")
1001 TimedeltaIndex(['1 days 00:03:00'], dtype='timedelta64[s]', freq=None)
1002 """
1003 arr = self._data.as_unit(unit)
1004 result = type(self)._simple_new(arr, name=self.name)
1005 result._freq = self.freq
1006 return result
1007
1008 def _with_freq(self, freq):
1009 # GH#29843
1010 if freq is None:
1011 pass
1012 elif isinstance(freq, BaseOffset):
1013 if self.dtype.kind == "m" and not isinstance(freq, (Tick, Day)):
1014 raise TypeError("TimedeltaArray/Index freq must be a Tick")
1015 elif freq == "infer":
1016 freq = to_offset(self._inferred_freq_str)
1017 else:
1018 raise ValueError(f"Invalid frequency: {freq!r}")
1019
1020 arr = self._data.view()
1021 result = type(self)._simple_new(arr, name=self._name)
1022 result._freq = freq
1023 return result
1024
1025 @property
1026 def values(self) -> np.ndarray:
1027 # NB: For Datetime64TZ this is lossy
1028 if isinstance(self.dtype, DatetimeTZDtype):
1029 warnings.warn(
1030 "DatetimeIndex.values returning an ndarray that drops "
1031 "timezone information is deprecated. In a future version, "
1032 "this will return the underlying DatetimeArray instead. "
1033 "Use 'DatetimeIndex.tz_convert(None).to_numpy()' to get a NumPy array "
1034 "of UTC values, or 'DatetimeIndex.array' to get the ExtensionArray.\n"
1035 "See https://pandas.pydata.org/docs/dev/whatsnew/v3.1.0.html#whatsnew-310-deprecations-values"
1036 " for more details.",
1037 Pandas4Warning,
1038 # TODO bump this to stacklevel=2 in a future version
1039 stacklevel=1,
1040 )
1041 data = self._data._ndarray
1042 data = data.view()
1043 data.flags.writeable = False
1044 return data
1045
1046 def shift(self, periods: int = 1, freq=None) -> Self:
1047 """
1048 Shift index by desired number of time frequency increments.
1049 This method is for shifting the values of datetime-like indexes
1050 by a specified time increment a given number of times.
1051
1052 Parameters
1053 ----------
1054 periods : int, default 1
1055 Number of periods (or increments) to shift by,
1056 can be positive or negative.
1057 freq : pandas.DateOffset, pandas.Timedelta or string, optional
1058 Frequency increment to shift by.
1059 If None, the index is shifted by its own `freq` attribute.
1060 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
1061
1062 Returns
1063 -------
1064 pandas.DatetimeIndex
1065 Shifted index.
1066
1067 See Also
1068 --------
1069 Index.shift : Shift values of Index.
1070 PeriodIndex.shift : Shift values of PeriodIndex.
1071 """
1072 if freq is not None and freq != self.freq:
1073 if isinstance(freq, str):
1074 freq = to_offset(freq)
1075 offset = periods * freq
1076 return self + offset
1077
1078 if periods == 0 or len(self) == 0:
1079 # GH#14811 empty case
1080 return self.copy()
1081
1082 if self.freq is None:
1083 raise NullFrequencyError("Cannot shift with no freq")
1084
1085 start = self[0] + periods * self.freq
1086 end = self[-1] + periods * self.freq
1087
1088 # Note: in the DatetimeTZ case, _generate_range will infer the
1089 # appropriate timezone from `start` and `end`, so tz does not need
1090 # to be passed explicitly.
1091 arr = self._data._generate_range(
1092 start=start, end=end, periods=None, freq=self.freq, unit=self.unit
1093 )
1094 result = type(self)._simple_new(arr, name=self.name)
1095 result._freq = self.freq
1096 return result
1097
1098 @cache_readonly
1099 def _inferred_freq_str(self) -> str | None:
1100 """
1101 Internal version of inferred_freq without deprecation warning.
1102 """
1103 return self._data._inferred_freq_str
1104
1105 @cache_readonly
1106 def inferred_freq(self) -> str | BaseOffset | None:
1107 """
1108 Return the inferred frequency of the index.
1109
1110 .. deprecated:: 3.1.0
1111 A future version of pandas will return a :class:`BaseOffset` instead of
1112 a string. Use
1113 ``pd.set_option('future.infer_freq_returns_offset', True)`` to opt
1114 in to the future behavior.
1115
1116 Attempts to determine the frequency of the index by analyzing the
1117 differences between consecutive values using ``infer_freq``.
1118
1119 Returns
1120 -------
1121 str or None
1122 A string representing a frequency generated by ``infer_freq``.
1123 Returns ``None`` if the frequency cannot be inferred.
1124
1125 See Also
1126 --------
1127 DatetimeIndex.freqstr : Return the frequency object as a string if it's set,
1128 otherwise ``None``.
1129
1130 Examples
1131 --------
1132 For ``DatetimeIndex``:
1133
1134 >>> idx = pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"])
1135 >>> idx.inferred_freq # doctest: +SKIP
1136 '2D'
1137
1138 For ``TimedeltaIndex``:
1139
1140 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"])
1141 >>> tdelta_idx
1142 TimedeltaIndex(['0 days', '10 days', '20 days'],
1143 dtype='timedelta64[us]', freq=None)
1144 >>> tdelta_idx.inferred_freq # doctest: +SKIP
1145 '10D'
1146 """
1147 return frequencies.maybe_convert_inferred_freq(
1148 self._inferred_freq_str, "inferred_freq"
1149 )
1150
1151 # --------------------------------------------------------------------
1152 # Set Operation Methods
1153
1154 @cache_readonly
1155 def _as_range_index(self) -> RangeIndex:
1156 # Convert our i8 representations to RangeIndex
1157 # Caller is responsible for checking isinstance(self.freq, Tick)
1158 freq = cast("Tick", self.freq)
1159 tick = Timedelta(freq).as_unit(self.unit)._value
1160 rng = range(self[0]._value, self[-1]._value + tick, tick)
1161 return RangeIndex(rng)
1162
1163 def _can_range_setop(self, other) -> bool:
1164 return isinstance(self.freq, Tick) and isinstance(other.freq, Tick)
1165
1166 def _wrap_range_setop(self, other, res_i8) -> Self:
1167 new_freq = None
1168 if not len(res_i8):
1169 # RangeIndex defaults to step=1, which we don't want.
1170 new_freq = self.freq
1171 elif isinstance(res_i8, RangeIndex):
1172 new_freq = to_offset(
1173 Timedelta(res_i8.step, unit=self.unit).as_unit(self.unit)
1174 )
1175
1176 # TODO(GH#41493): we cannot just do
1177 # type(self._data)(res_i8.values, dtype=self.dtype, freq=new_freq)
1178 # because test_setops_preserve_freq fails with _validate_frequency raising.
1179 # This raising is incorrect, as 'on_freq' is incorrect. This will
1180 # be fixed by GH#41493
1181 res_values = res_i8.values.view(self._data._ndarray.dtype)
1182 arr = type(self._data)._simple_new(
1183 # error: Argument "dtype" to "_simple_new" of "DatetimeArray" has
1184 # incompatible type "Union[dtype[Any], ExtensionDtype]"; expected
1185 # "Union[dtype[datetime64], DatetimeTZDtype]"
1186 res_values,
1187 dtype=self.dtype, # type: ignore[arg-type]
1188 )
1189 result = cast("Self", self._wrap_setop_result(other, arr))
1190 result._freq = new_freq
1191 return result
1192
1193 def _range_intersect(self, other, sort) -> Self:
1194 # Dispatch to RangeIndex intersection logic.
1195 left = self._as_range_index
1196 right = other._as_range_index
1197 res_i8 = left.intersection(right, sort=sort)
1198 return self._wrap_range_setop(other, res_i8)
1199
1200 def _range_union(self, other, sort) -> Self:
1201 # Dispatch to RangeIndex union logic.
1202 left = self._as_range_index
1203 right = other._as_range_index
1204 res_i8 = left.union(right, sort=sort)
1205 return self._wrap_range_setop(other, res_i8)
1206
1207 def _intersection(self, other: Index, sort: bool = False) -> Index:
1208 """
1209 intersection specialized to the case with matching dtypes and both non-empty.
1210 """
1211 other = cast("DatetimeTimedeltaMixin", other)
1212
1213 if self._can_range_setop(other):
1214 return self._range_intersect(other, sort=sort)
1215
1216 if not self._can_fast_intersect(other):
1217 result = Index._intersection(self, other, sort=sort)
1218 # We need to invalidate the freq because Index._intersection
1219 # uses _shallow_copy on a view of self._data, which will preserve
1220 # self.freq if we're not careful.
1221 # At this point we should have result.dtype == self.dtype
1222 # and type(result) is type(self._data)
1223 result = self._wrap_setop_result(other, result)
1224 # error: "Index" has no attribute "_with_freq"; maybe "_with_infer"?
1225 return result._with_freq(None)._with_freq("infer") # type: ignore[attr-defined]
1226
1227 else:
1228 return self._fast_intersect(other, sort)
1229
1230 def _fast_intersect(self, other, sort):
1231 # to make our life easier, "sort" the two ranges
1232 if self[0] <= other[0]:
1233 left, right = self, other
1234 else:
1235 left, right = other, self
1236
1237 # after sorting, the intersection always starts with the right index
1238 # and ends with the index of which the last elements is smallest
1239 end = min(left[-1], right[-1])
1240 start = right[0]
1241
1242 if end < start:
1243 return self[:0]
1244
1245 lslice = slice(*left.slice_locs(start, end))
1246 arr = left._values[lslice]
1247 result = type(self)._simple_new(arr, name=self.name)
1248 result._freq = self.freq
1249 return result
1250
1251 def _can_fast_intersect(self, other: Self) -> bool:
1252 # Note: we only get here with len(self) > 0 and len(other) > 0
1253 if self.freq is None:
1254 return False
1255
1256 elif other.freq != self.freq:
1257 return False
1258
1259 elif not self.is_monotonic_increasing:
1260 # Because freq is not None, we must then be monotonic decreasing
1261 return False
1262
1263 freq = self.freq
1264
1265 # GH#44025 DateOffset (RelativeDeltaOffset) has no fixed stride,
1266 # so matching freq alone doesn't guarantee alignment.
1267 if isinstance(freq, RelativeDeltaOffset):
1268 return False
1269
1270 # Note we are assuming away Ticks, as those go through _range_intersect.
1271 # For |n| != 1 two same-freq ranges can be offset by a non-multiple of
1272 # n (e.g. "2ME" starting in different months), so they need not line
1273 # up. Rather than verify the phase, fall back to the general (slower)
1274 # intersection. GH#42104, GH#44025
1275 if freq.n != 1:
1276 return False
1277
1278 # Below here we need actual timestamp values; cache them once
1279 # since __getitem__ is relatively expensive on DatetimeIndex.
1280 left_start = self[0]
1281 right_start = other[0]
1282
1283 # GH#44025 For DatetimeIndex, both indices must share the same
1284 # wall-clock time-of-day to guarantee alignment, as non-Tick
1285 # offsets preserve wall time. (TimedeltaIndex elements are
1286 # Timedelta, which have no time-of-day concept.)
1287 if isinstance(left_start, Timestamp):
1288 if (left_start.time(), left_start.nanosecond) != (
1289 right_start.time(),
1290 right_start.nanosecond,
1291 ):
1292 return False
1293
1294 # GH#44025 Week(weekday=None) generates dates that depend on the
1295 # start date: check that both indexes fall on the same day-of-week.
1296 if isinstance(freq, Week) and freq.weekday is None:
1297 if (left_start.toordinal() - right_start.toordinal()) % 7 != 0:
1298 return False
1299
1300 return True
1301
1302 def _can_fast_union(self, other: Self) -> bool:
1303 # Assumes that type(self) == type(other), as per the annotation
1304 # The ability to fast_union also implies that `freq` should be
1305 # retained on union.
1306 freq = self.freq
1307
1308 if freq is None or freq != other.freq:
1309 return False
1310
1311 if not self.is_monotonic_increasing:
1312 # Because freq is not None, we must then be monotonic decreasing
1313 # TODO: do union on the reversed indexes?
1314 return False
1315
1316 if len(self) == 0 or len(other) == 0:
1317 # only reached via union_many
1318 return True
1319
1320 # to make our life easier, "sort" the two ranges
1321 if self[0] <= other[0]:
1322 left, right = self, other
1323 else:
1324 left, right = other, self
1325
1326 right_start = right[0]
1327 left_end = left[-1]
1328
1329 # Only need to "adjoin", not overlap
1330 return (right_start == left_end + freq) or right_start in left
1331
1332 def _fast_union(self, other: Self) -> Self:
1333 # Caller is responsible for ensuring self and other are non-empty
1334 # and that the result is monotonic, so that it retains self.freq.
1335
1336 # to make our life easier, "sort" the two ranges
1337 if self[0] <= other[0]:
1338 left, right = self, other
1339 else:
1340 left, right = other, self
1341
1342 left_end = left[-1]
1343 right_end = right[-1]
1344
1345 # concatenate
1346 if left_end < right_end:
1347 loc = right.searchsorted(left_end, side="right")
1348 right_chunk = right._values[loc:]
1349 dates = concat_compat([left._values, right_chunk])
1350 # The can_fast_union check ensures that the result.freq
1351 # should match self.freq
1352 assert isinstance(dates, type(self._data))
1353 result = type(self)._simple_new(dates)
1354 result._freq = self.freq
1355 return result
1356 else:
1357 return left
1358
1359 def _union(self, other, sort):
1360 # We are called by `union`, which is responsible for this validation
1361 assert isinstance(other, type(self))
1362 assert self.dtype == other.dtype
1363
1364 if self._can_range_setop(other):
1365 return self._range_union(other, sort=sort)
1366
1367 if self._can_fast_union(other) and (sort is not False or self[0] <= other[0]):
1368 # the _can_fast_union check ensures that result.freq == self.freq.
1369 # With sort=False and self starting after other, the result would
1370 # instead be non-monotonic and carry no freq, so we leave that case
1371 # to the generic path below. GH#66322
1372 return self._fast_union(other)
1373 else:
1374 # super()._union can return an ArrayLike; wrap into an Index first
1375 result = self._wrap_setop_result(other, super()._union(other, sort))
1376 return result._with_freq("infer") # type: ignore[attr-defined]
1377
1378 # --------------------------------------------------------------------
1379 # Join Methods
1380
1381 def _get_join_freq(self, other):
1382 """
1383 Get the freq to attach to the result of a join operation.
1384 """
1385 freq = None
1386 if self._can_fast_union(other):
1387 freq = self.freq
1388 return freq
1389
1390 def _wrap_join_result(
1391 self,
1392 joined,
1393 other,
1394 lidx: npt.NDArray[np.intp] | None,
1395 ridx: npt.NDArray[np.intp] | None,
1396 how: JoinHow,
1397 ) -> tuple[Self, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
1398 assert other.dtype == self.dtype, (other.dtype, self.dtype)
1399 join_index, lidx, ridx = super()._wrap_join_result(
1400 joined, other, lidx, ridx, how
1401 )
1402 join_index._freq = self._get_join_freq(other)
1403 return join_index, lidx, ridx
1404
1405 def _get_engine_target(self) -> np.ndarray:
1406 # engine methods and libjoin methods need dt64/td64 values cast to i8
1407 return self._data._ndarray.view("i8")
1408
1409 def _from_join_target(self, result: np.ndarray):
1410 # view e.g. i8 back to M8[ns]
1411 result = result.view(self._data._ndarray.dtype)
1412 return self._data._from_backing_data(result)
1413
1414 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
1415 if (
1416 self.is_monotonic_increasing
1417 and isinstance(label, (Timestamp, Timedelta))
1418 and abbrev_to_npy_unit(label.unit) > abbrev_to_npy_unit(self.unit)
1419 ):
1420 # For non-matching units we can safely round down (with side=right)
1421 # This is needed for GH#63262
1422 if side == "right":
1423 label = label.as_unit(self.unit) # this should always be a round-down
1424 else:
1425 # round up
1426 label = label.ceil(self.unit).as_unit(self.unit)
1427
1428 return super()._searchsorted_monotonic(label, side)
1429
1430 # --------------------------------------------------------------------
1431 # List-like Methods
1432
1433 def _getitem_slice(self, slobj: slice) -> Self:
1434 result = super()._getitem_slice(slobj)
1435 result._freq = self._get_getitem_freq(slobj)
1436 return result
1437
1438 def __getitem__(self, key):
1439 result = super().__getitem__(key)
1440 if isinstance(result, type(self)):
1441 result._freq = self._get_getitem_freq(key)
1442 return result
1443
1444 def _get_getitem_freq(self, key) -> BaseOffset | None:
1445 """
1446 Find the `freq` attribute to assign to the result of a __getitem__ lookup.
1447 """
1448 key = check_array_indexer(self._data, key) # maybe ndarray[bool] -> slice
1449 freq = None
1450 if isinstance(key, slice):
1451 if self.freq is not None and key.step is not None:
1452 freq = key.step * self.freq
1453 else:
1454 freq = self.freq
1455 elif key is Ellipsis:
1456 # GH#21282 indexing with Ellipsis is similar to a full slice,
1457 # should preserve `freq` attribute
1458 freq = self.freq
1459 elif com.is_bool_indexer(key):
1460 new_key = lib.maybe_booleans_to_slice(key.view(np.uint8))
1461 if isinstance(new_key, slice):
1462 return self._get_getitem_freq(new_key)
1463 return freq
1464
1465 def _concat(self, to_concat: list[Index], name: Hashable) -> Index:
1466 result = super()._concat(to_concat, name)
1467
1468 # GH#3232: If the concat result is evenly spaced, we can retain the
1469 # original frequency
1470 obj = cast("DatetimeTimedeltaMixin", to_concat[0])
1471 to_concat_nonempty = cast(
1472 "list[DatetimeTimedeltaMixin]",
1473 [idx for idx in to_concat if len(idx)],
1474 )
1475 if (
1476 isinstance(result, type(self))
1477 and obj.freq is not None
1478 and all(idx.freq == obj.freq for idx in to_concat_nonempty)
1479 ):
1480 freq = obj.freq
1481 tz = getattr(self.dtype, "tz", None)
1482 if (
1483 isinstance(freq, Tick) or (tz is None and isinstance(freq, Day))
1484 ) and all(idx.unit == self.unit for idx in to_concat_nonempty):
1485 # freq is a fixed delta in the stored i8 representation, so we
1486 # can check boundary continuity without boxing endpoints to
1487 # Timestamps and doing per-pair offset arithmetic.
1488 step = Timedelta(freq).as_unit(self.unit)._value
1489 i8s = [idx._data._ndarray.view("i8") for idx in to_concat_nonempty]
1490 evenly_spaced = all(a[-1] + step == b[0] for a, b in pairwise(i8s))
1491 else:
1492 evenly_spaced = all(
1493 pair[0][-1] + freq == pair[1][0]
1494 for pair in pairwise(to_concat_nonempty)
1495 )
1496 if evenly_spaced:
1497 result._freq = freq
1498
1499 return result
1500
1501 def _get_delete_freq(self, loc: int | slice | Sequence[int]):
1502 """
1503 Find the `freq` for self.delete(loc).
1504 """
1505 freq = None
1506 if self.freq is not None:
1507 if is_integer(loc):
1508 if loc in (0, -len(self), -1, len(self) - 1):
1509 freq = self.freq
1510 else:
1511 if is_list_like(loc):
1512 # error: Incompatible types in assignment (expression has
1513 # type "Union[slice, ndarray]", variable has type
1514 # "Union[int, slice, Sequence[int]]")
1515 loc = lib.maybe_indices_to_slice( # type: ignore[assignment]
1516 np.asarray(loc, dtype=np.intp), len(self)
1517 )
1518 if isinstance(loc, slice) and loc.step in (1, None):
1519 if loc.start in (0, None) or loc.stop in (len(self), None):
1520 freq = self.freq
1521 return freq
1522
1523 def _get_insert_freq(self, loc: int, item):
1524 """
1525 Find the `freq` for self.insert(loc, item).
1526 """
1527 value = self._data._validate_scalar(item)
1528 item = self._data._box_func(value)
1529
1530 freq = None
1531 if self.freq is not None:
1532 # freq can be preserved on edge cases
1533 if self.size:
1534 if item is NaT:
1535 pass
1536 elif loc in (0, -len(self)) and item + self.freq == self[0]:
1537 freq = self.freq
1538 elif (loc == len(self)) and item - self.freq == self[-1]:
1539 freq = self.freq
1540 # Adding a single item to an empty index may preserve freq
1541 elif isinstance(self.freq, Tick):
1542 # all TimedeltaIndex cases go through here; is_on_offset
1543 # would raise TypeError
1544 freq = self.freq
1545 elif self.freq.is_on_offset(item):
1546 freq = self.freq
1547 return freq
1548
1549 def delete(self, loc) -> Self:
1550 """
1551 Make new Index with passed location(-s) deleted.
1552
1553 Parameters
1554 ----------
1555 loc : int or list of int
1556 Location of item(-s) which will be deleted.
1557 Use a list of locations to delete more than one value at the same time.
1558
1559 Returns
1560 -------
1561 Index
1562 Will be same type as self, except for RangeIndex.
1563
1564 See Also
1565 --------
1566 numpy.delete : Delete any rows and column from NumPy array (ndarray).
1567
1568 Examples
1569 --------
1570 >>> idx = pd.Index(["a", "b", "c"])
1571 >>> idx.delete(1)
1572 Index(['a', 'c'], dtype='str')
1573 >>> idx = pd.Index(["a", "b", "c"])
1574 >>> idx.delete([0, 2])
1575 Index(['b'], dtype='str')
1576 """
1577 result = super().delete(loc)
1578 result._freq = self._get_delete_freq(loc)
1579 return result
1580
1581 def insert(self, loc: int, item):
1582 """
1583 Make new Index inserting new item at location.
1584 Follows Python numpy.insert semantics for negative values.
1585
1586 Parameters
1587 ----------
1588 loc : int
1589 The integer location where the new item will be inserted.
1590 item : object
1591 The new item to be inserted into the Index.
1592
1593 Returns
1594 -------
1595 Index
1596 Returns a new Index object resulting from inserting the specified item at
1597 the specified location within the original Index.
1598
1599 See Also
1600 --------
1601 Index.append : Append a collection of Indexes together.
1602
1603 Examples
1604 --------
1605 >>> idx = pd.Index(["a", "b", "c"])
1606 >>> idx.insert(1, "x")
1607 Index(['a', 'x', 'b', 'c'], dtype='str')
1608 """
1609 result = super().insert(loc, item)
1610 if isinstance(result, type(self)):
1611 # i.e. parent class method did not cast
1612 result._freq = self._get_insert_freq(loc, item)
1613 return result
1614
1615 # --------------------------------------------------------------------
1616 # NDArray-Like Methods
1617
1618 def take(
1619 self,
1620 indices,
1621 axis: Axis = 0,
1622 allow_fill: bool | lib.NoDefault = lib.no_default,
1623 fill_value=lib.no_default,
1624 **kwargs,
1625 ) -> Self:
1626 """
1627 Return a new Index of the values selected by the indices.
1628
1629 For internal compatibility with numpy arrays.
1630
1631 Parameters
1632 ----------
1633 indices : array-like
1634 Indices to be taken.
1635 axis : {0 or 'index'}, optional
1636 The axis over which to select values, always 0 or 'index'.
1637 allow_fill : bool, optional
1638 How to handle negative values in `indices`.
1639
1640 * False: negative values in `indices` indicate positional indices
1641 from the right, matching :func:`numpy.take`.
1642 * True: negative values in `indices` indicate missing values. ``-1``
1643 entries are set to ``fill_value`` (defaulting to ``NaT`` if not
1644 supplied). Any other negative values raise a ``ValueError``.
1645 * Not supplied: defaults to ``allow_fill=False`` unless
1646 ``fill_value`` is explicitly provided, in which case fill
1647 semantics apply (``allow_fill=True``).
1648 fill_value : scalar, optional
1649 If fill semantics apply (see ``allow_fill``), indices specified by
1650 ``-1`` are filled with ``fill_value``. Passing ``fill_value=None``
1651 is equivalent to passing ``self._na_value`` (``NaT``).
1652 **kwargs
1653 Required for compatibility with numpy.
1654
1655 Returns
1656 -------
1657 Index
1658 An index formed of elements at the given indices. Will be the same
1659 type as self, except for RangeIndex.
1660
1661 See Also
1662 --------
1663 numpy.ndarray.take: Return an array formed from the
1664 elements of a at the given indices.
1665
1666 Examples
1667 --------
1668 >>> idx = pd.date_range("2024-01-01", periods=3)
1669 >>> idx.take([2, 1, 0])
1670 DatetimeIndex(['2024-01-03', '2024-01-02', '2024-01-01'],
1671 dtype='datetime64[us]', freq='-1D')
1672 """
1673 nv.validate_take((), kwargs)
1674 indices = np.asarray(indices, dtype=np.intp)
1675
1676 result = NDArrayBackedExtensionIndex.take(
1677 self, indices, axis, allow_fill, fill_value, **kwargs
1678 )
1679
1680 maybe_slice = lib.maybe_indices_to_slice(indices, len(self))
1681 if isinstance(maybe_slice, slice):
1682 freq = self._get_getitem_freq(maybe_slice)
1683 result._freq = freq
1684 return result