1"""
2Base and utility classes for tseries type pandas objects.
3"""
4
5from __future__ import annotations
6
7from abc import (
8 ABC,
9 abstractmethod,
10)
11from typing import (
12 TYPE_CHECKING,
13 Any,
14 Literal,
15 Self,
16 cast,
17 final,
18)
19
20import numpy as np
21
22from pandas._libs import (
23 NaT,
24 lib,
25)
26from pandas._libs.tslibs import (
27 BaseOffset,
28 Resolution,
29 Tick,
30 Timedelta,
31 Timestamp,
32 parsing,
33 to_offset,
34)
35from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
36from pandas.compat.numpy import function as nv
37from pandas.errors import (
38 InvalidIndexError,
39 NullFrequencyError,
40 OutOfBoundsDatetime,
41 OutOfBoundsTimedelta,
42)
43from pandas.util._decorators import (
44 cache_readonly,
45)
46
47from pandas.core.dtypes.common import (
48 is_integer,
49 is_list_like,
50)
51from pandas.core.dtypes.concat import concat_compat
52from pandas.core.dtypes.dtypes import (
53 CategoricalDtype,
54 PeriodDtype,
55)
56
57from pandas.core.arrays import (
58 DatetimeArray,
59 ExtensionArray,
60 PeriodArray,
61 TimedeltaArray,
62)
63import pandas.core.common as com
64import pandas.core.indexes.base as ibase
65from pandas.core.indexes.base import (
66 Index,
67)
68from pandas.core.indexes.extension import NDArrayBackedExtensionIndex
69from pandas.core.indexes.range import RangeIndex
70from pandas.core.tools.timedeltas import to_timedelta
71
72if TYPE_CHECKING:
73 from collections.abc import Sequence
74 from datetime import datetime
75
76 from pandas._typing import (
77 Axis,
78 JoinHow,
79 TimeUnit,
80 npt,
81 )
82
83 from pandas import CategoricalIndex
84
85_index_doc_kwargs = dict(ibase._index_doc_kwargs)
86
87
88class DatetimeIndexOpsMixin(NDArrayBackedExtensionIndex, ABC):
89 """
90 Common ops mixin to support a unified interface datetimelike Index.
91 """
92
93 _can_hold_strings = False
94 _data: DatetimeArray | TimedeltaArray | PeriodArray
95
96 def mean(self, *, skipna: bool = True, axis: int | None = 0):
97 """
98 Return the mean value of the Array.
99
100 Parameters
101 ----------
102 skipna : bool, default True
103 Whether to ignore any NaT elements.
104 axis : int, optional, default 0
105 Axis for the function to be applied on.
106
107 Returns
108 -------
109 scalar
110 Timestamp or Timedelta.
111
112 See Also
113 --------
114 numpy.ndarray.mean : Returns the average of array elements along a given axis.
115 Series.mean : Return the mean value in a Series.
116
117 Notes
118 -----
119 mean is only defined for Datetime and Timedelta dtypes, not for Period.
120
121 Examples
122 --------
123 For :class:`pandas.DatetimeIndex`:
124
125 >>> idx = pd.date_range("2001-01-01 00:00", periods=3)
126 >>> idx
127 DatetimeIndex(['2001-01-01', '2001-01-02', '2001-01-03'],
128 dtype='datetime64[us]', freq='D')
129 >>> idx.mean()
130 Timestamp('2001-01-02 00:00:00')
131
132 For :class:`pandas.TimedeltaIndex`:
133
134 >>> tdelta_idx = pd.to_timedelta([1, 2, 3], unit="D")
135 >>> tdelta_idx
136 TimedeltaIndex(['1 days', '2 days', '3 days'],
137 dtype='timedelta64[s]', freq=None)
138 >>> tdelta_idx.mean()
139 Timedelta('2 days 00:00:00')
140 """
141 return self._data.mean(skipna=skipna, axis=axis)
142
143 @property
144 def freq(self) -> BaseOffset | None:
145 """
146 Return the frequency object if it is set, otherwise None.
147
148 To learn more about the frequency strings, please see
149 :ref:`this link<timeseries.offset_aliases>`.
150
151 See Also
152 --------
153 DatetimeIndex.freq : Return the frequency object if it is set, otherwise None.
154 PeriodIndex.freq : Return the frequency object if it is set, otherwise None.
155
156 Examples
157 --------
158 >>> datetimeindex = pd.date_range(
159 ... "2022-02-22 02:22:22", periods=10, tz="America/Chicago", freq="h"
160 ... )
161 >>> datetimeindex
162 DatetimeIndex(['2022-02-22 02:22:22-06:00', '2022-02-22 03:22:22-06:00',
163 '2022-02-22 04:22:22-06:00', '2022-02-22 05:22:22-06:00',
164 '2022-02-22 06:22:22-06:00', '2022-02-22 07:22:22-06:00',
165 '2022-02-22 08:22:22-06:00', '2022-02-22 09:22:22-06:00',
166 '2022-02-22 10:22:22-06:00', '2022-02-22 11:22:22-06:00'],
167 dtype='datetime64[us, America/Chicago]', freq='h')
168 >>> datetimeindex.freq
169 <Hour>
170 """
171 return self._data.freq
172
173 @freq.setter
174 def freq(self, value) -> None:
175 # error: Property "freq" defined in "PeriodArray" is read-only [misc]
176 self._data.freq = value # type: ignore[misc]
177
178 @property
179 def asi8(self) -> npt.NDArray[np.int64]:
180 return self._data.asi8
181
182 @property
183 def freqstr(self) -> str:
184 """
185 Return the frequency object as a string if it's set, otherwise None.
186
187 See Also
188 --------
189 DatetimeIndex.inferred_freq : Returns a string representing a frequency
190 generated by infer_freq.
191
192 Examples
193 --------
194 For DatetimeIndex:
195
196 >>> idx = pd.DatetimeIndex(["1/1/2020 10:00:00+00:00"], freq="D")
197 >>> idx.freqstr
198 'D'
199
200 The frequency can be inferred if there are more than 2 points:
201
202 >>> idx = pd.DatetimeIndex(
203 ... ["2018-01-01", "2018-01-03", "2018-01-05"], freq="infer"
204 ... )
205 >>> idx.freqstr
206 '2D'
207
208 For PeriodIndex:
209
210 >>> idx = pd.PeriodIndex(["2023-1", "2023-2", "2023-3"], freq="M")
211 >>> idx.freqstr
212 'M'
213 """
214 from pandas import PeriodIndex
215
216 if self._data.freqstr is not None and isinstance(
217 self._data, (PeriodArray, PeriodIndex)
218 ):
219 freq = PeriodDtype(self._data.freq)._freqstr
220 return freq
221 else:
222 return self._data.freqstr # type: ignore[return-value]
223
224 @cache_readonly
225 @abstractmethod
226 def _resolution_obj(self) -> Resolution: ...
227
228 @cache_readonly
229 def resolution(self) -> str:
230 """
231 Returns day, hour, minute, second, millisecond or microsecond
232 """
233 return self._data.resolution
234
235 # ------------------------------------------------------------------------
236
237 @cache_readonly
238 def hasnans(self) -> bool:
239 return self._data._hasna
240
241 def equals(self, other: Any) -> bool:
242 """
243 Determines if two Index objects contain the same elements.
244 """
245 if self.is_(other):
246 return True
247
248 if not isinstance(other, Index):
249 return False
250 elif other.dtype.kind in "iufc":
251 return False
252 elif not isinstance(other, type(self)):
253 should_try = False
254 inferable = self._data._infer_matches
255 if other.dtype == object:
256 should_try = other.inferred_type in inferable
257 elif isinstance(other.dtype, CategoricalDtype):
258 other = cast("CategoricalIndex", other)
259 should_try = other.categories.inferred_type in inferable
260
261 if should_try:
262 try:
263 other = type(self)(other)
264 except (ValueError, TypeError, OverflowError):
265 # e.g.
266 # ValueError -> cannot parse str entry, or OutOfBoundsDatetime
267 # TypeError -> trying to convert IntervalIndex to DatetimeIndex
268 # OverflowError -> Index([very_large_timedeltas])
269 return False
270
271 if type(self) != type(other):
272 return False
273 elif self.dtype == other.dtype:
274 return np.array_equal(self.asi8, other.asi8)
275 elif (self.dtype.kind == "M" and self.tz == other.tz) or self.dtype.kind == "m": # type: ignore[attr-defined]
276 # different units, otherwise matching
277 try:
278 # TODO: do this at the EA level?
279 left, right = self._data._ensure_matching_resos(other._data) # type: ignore[union-attr]
280 except (OutOfBoundsDatetime, OutOfBoundsTimedelta):
281 return False
282 else:
283 return np.array_equal(left.view("i8"), right.view("i8"))
284 return False
285
286 def __contains__(self, key: Any) -> bool:
287 """
288 Return a boolean indicating whether the provided key is in the index.
289
290 Parameters
291 ----------
292 key : label
293 The key to check if it is present in the index.
294
295 Returns
296 -------
297 bool
298 Whether the key search is in the index.
299
300 Raises
301 ------
302 TypeError
303 If the key is not hashable.
304
305 See Also
306 --------
307 Index.isin : Returns an ndarray of boolean dtype indicating whether the
308 list-like key is in the index.
309
310 Examples
311 --------
312 >>> idx = pd.Index([1, 2, 3, 4])
313 >>> idx
314 Index([1, 2, 3, 4], dtype='int64')
315 >>> 2 in idx
316 True
317 >>> 6 in idx
318 False
319 """
320 hash(key)
321 try:
322 self.get_loc(key)
323 except (KeyError, TypeError, ValueError, InvalidIndexError):
324 return False
325 return True
326
327 def _convert_tolerance(self, tolerance, target):
328 tolerance = np.asarray(to_timedelta(tolerance).to_numpy())
329 return super()._convert_tolerance(tolerance, target)
330
331 # --------------------------------------------------------------------
332 # Rendering Methods
333 _default_na_rep = "NaT"
334
335 def _format_with_header(
336 self, *, header: list[str], na_rep: str, date_format: str | None = None
337 ) -> list[str]:
338 # TODO: not reached in tests 2023-10-11
339 # matches base class except for whitespace padding and date_format
340 return header + list(
341 self._get_values_for_csv(na_rep=na_rep, date_format=date_format)
342 )
343
344 @property
345 def _formatter_func(self):
346 return self._data._formatter()
347
348 def _format_attrs(self):
349 """
350 Return a list of tuples of the (attr,formatted_value).
351 """
352 attrs = super()._format_attrs()
353 for attrib in self._attributes:
354 # iterating over _attributes prevents us from doing this for PeriodIndex
355 if attrib == "freq":
356 freq = self.freqstr
357 if freq is not None:
358 freq = repr(freq) # e.g. D -> 'D'
359 attrs.append(("freq", freq))
360 return attrs
361
362 def _summary(self, name=None) -> str:
363 """
364 Return a summarized representation.
365
366 Parameters
367 ----------
368 name : str
369 name to use in the summary representation
370
371 Returns
372 -------
373 String with a summarized representation of the index
374 """
375 result = super()._summary(name=name)
376 if self.freq:
377 result += f"\nFreq: {self.freqstr}"
378
379 return result
380
381 # --------------------------------------------------------------------
382 # Indexing Methods
383
384 @final
385 def _can_partial_date_slice(self, reso: Resolution) -> bool:
386 # e.g. test_getitem_setitem_periodindex
387 # History of conversation GH#3452, GH#3931, GH#2369, GH#14826
388 return reso > self._resolution_obj
389 # NB: for DTI/PI, not TDI
390
391 def _parsed_string_to_bounds(self, reso: Resolution, parsed):
392 raise NotImplementedError
393
394 def _parse_with_reso(self, label: str) -> tuple[datetime, Resolution]:
395 # overridden by TimedeltaIndex
396 try:
397 if self.freq is None or hasattr(self.freq, "rule_code"):
398 freq = self.freq
399 except NotImplementedError:
400 freq = getattr(self, "freqstr", getattr(self, "inferred_freq", None))
401
402 freqstr: str | None
403 if freq is not None and not isinstance(freq, str):
404 freqstr = freq.rule_code
405 else:
406 freqstr = freq
407
408 if isinstance(label, np.str_):
409 # GH#45580
410 label = str(label)
411
412 parsed, reso_str = parsing.parse_datetime_string_with_reso(label, freqstr)
413 reso = Resolution.from_attrname(reso_str)
414 return parsed, reso
415
416 def _get_string_slice(self, key: str) -> slice | npt.NDArray[np.intp]:
417 # overridden by TimedeltaIndex
418 parsed, reso = self._parse_with_reso(key)
419 try:
420 return self._partial_date_slice(reso, parsed)
421 except KeyError as err:
422 raise KeyError(key) from err
423
424 @final
425 def _partial_date_slice(
426 self,
427 reso: Resolution,
428 parsed: datetime,
429 ) -> slice | npt.NDArray[np.intp]:
430 """
431 Parameters
432 ----------
433 reso : Resolution
434 parsed : datetime
435
436 Returns
437 -------
438 slice or ndarray[intp]
439 """
440 if not self._can_partial_date_slice(reso):
441 raise ValueError
442
443 t1, t2 = self._parsed_string_to_bounds(reso, parsed)
444 vals = self._data._ndarray
445 unbox = self._data._unbox
446
447 if self.is_monotonic_increasing:
448 if len(self) and (
449 (t1 < self[0] and t2 < self[0]) or (t1 > self[-1] and t2 > self[-1])
450 ):
451 # we are out of range
452 raise KeyError
453
454 # TODO: does this depend on being monotonic _increasing_?
455
456 # a monotonic (sorted) series can be sliced
457 left = vals.searchsorted(unbox(t1), side="left")
458 right = vals.searchsorted(unbox(t2), side="right")
459 return slice(left, right)
460
461 else:
462 lhs_mask = vals >= unbox(t1)
463 rhs_mask = vals <= unbox(t2)
464
465 # try to find the dates
466 return (lhs_mask & rhs_mask).nonzero()[0]
467
468 def _maybe_cast_slice_bound(self, label, side: str):
469 """
470 If label is a string, cast it to scalar type according to resolution.
471
472 Parameters
473 ----------
474 label : object
475 side : {'left', 'right'}
476
477 Returns
478 -------
479 label : object
480
481 Notes
482 -----
483 Value of `side` parameter should be validated in caller.
484 """
485 if isinstance(label, str):
486 try:
487 parsed, reso = self._parse_with_reso(label)
488 except ValueError as err:
489 # DTI -> parsing.DateParseError
490 # TDI -> 'unit abbreviation w/o a number'
491 # PI -> string cannot be parsed as datetime-like
492 self._raise_invalid_indexer("slice", label, err)
493
494 lower, upper = self._parsed_string_to_bounds(reso, parsed)
495 return lower if side == "left" else upper
496 elif not isinstance(label, self._data._recognized_scalars):
497 self._raise_invalid_indexer("slice", label)
498
499 return label
500
501 # --------------------------------------------------------------------
502 # Arithmetic Methods
503
504 def shift(self, periods: int = 1, freq=None) -> Self:
505 """
506 Shift index by desired number of time frequency increments.
507
508 This method is for shifting the values of datetime-like indexes
509 by a specified time increment a given number of times.
510
511 Parameters
512 ----------
513 periods : int, default 1
514 Number of periods (or increments) to shift by,
515 can be positive or negative.
516 freq : pandas.DateOffset, pandas.Timedelta or string, optional
517 Frequency increment to shift by.
518 If None, the index is shifted by its own `freq` attribute.
519 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
520
521 Returns
522 -------
523 pandas.DatetimeIndex
524 Shifted index.
525
526 See Also
527 --------
528 Index.shift : Shift values of Index.
529 PeriodIndex.shift : Shift values of PeriodIndex.
530 """
531 raise NotImplementedError
532
533 # --------------------------------------------------------------------
534
535 def _maybe_cast_listlike_indexer(self, keyarr):
536 """
537 Analogue to maybe_cast_indexer for get_indexer instead of get_loc.
538 """
539 try:
540 res = self._data._validate_listlike(keyarr, allow_object=True)
541 except (ValueError, TypeError):
542 if not isinstance(keyarr, ExtensionArray):
543 # e.g. we don't want to cast DTA to ndarray[object]
544 res = com.asarray_tuplesafe(keyarr)
545 # TODO: com.asarray_tuplesafe shouldn't cast e.g. DatetimeArray
546 else:
547 res = keyarr
548 return Index(res, dtype=res.dtype)
549
550
551class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, ABC):
552 """
553 Mixin class for methods shared by DatetimeIndex and TimedeltaIndex,
554 but not PeriodIndex
555 """
556
557 _data: DatetimeArray | TimedeltaArray
558 _comparables = ["name", "freq"]
559 _attributes = ["name", "freq"]
560
561 # Compat for frequency inference, see GH#23789
562 _is_monotonic_increasing = Index.is_monotonic_increasing
563 _is_monotonic_decreasing = Index.is_monotonic_decreasing
564 _is_unique = Index.is_unique
565
566 @property
567 def unit(self) -> TimeUnit:
568 return self._data.unit
569
570 def as_unit(self, unit: TimeUnit) -> Self:
571 """
572 Convert to a dtype with the given unit resolution.
573
574 This method is for converting the dtype of a ``DatetimeIndex`` or
575 ``TimedeltaIndex`` to a new dtype with the given unit
576 resolution/precision.
577
578 Parameters
579 ----------
580 unit : {'s', 'ms', 'us', 'ns'}
581
582 Returns
583 -------
584 same type as self
585 Converted to the specified unit.
586
587 See Also
588 --------
589 Timestamp.as_unit : Convert to the given unit.
590 Timedelta.as_unit : Convert to the given unit.
591 DatetimeIndex.as_unit : Convert to the given unit.
592 TimedeltaIndex.as_unit : Convert to the given unit.
593
594 Examples
595 --------
596 For :class:`pandas.DatetimeIndex`:
597
598 >>> idx = pd.DatetimeIndex(["2020-01-02 01:02:03.004005006"])
599 >>> idx
600 DatetimeIndex(['2020-01-02 01:02:03.004005006'],
601 dtype='datetime64[ns]', freq=None)
602 >>> idx.as_unit("s")
603 DatetimeIndex(['2020-01-02 01:02:03'], dtype='datetime64[s]', freq=None)
604
605 For :class:`pandas.TimedeltaIndex`:
606
607 >>> tdelta_idx = pd.to_timedelta(["1 day 3 min 2 us 42 ns"])
608 >>> tdelta_idx
609 TimedeltaIndex(['1 days 00:03:00.000002042'],
610 dtype='timedelta64[ns]', freq=None)
611 >>> tdelta_idx.as_unit("s")
612 TimedeltaIndex(['1 days 00:03:00'], dtype='timedelta64[s]', freq=None)
613 """
614 arr = self._data.as_unit(unit)
615 return type(self)._simple_new(arr, name=self.name)
616
617 def _with_freq(self, freq):
618 arr = self._data._with_freq(freq)
619 return type(self)._simple_new(arr, name=self._name)
620
621 @property
622 def values(self) -> np.ndarray:
623 # NB: For Datetime64TZ this is lossy
624 data = self._data._ndarray
625 data = data.view()
626 data.flags.writeable = False
627 return data
628
629 def shift(self, periods: int = 1, freq=None) -> Self:
630 """
631 Shift index by desired number of time frequency increments.
632 This method is for shifting the values of datetime-like indexes
633 by a specified time increment a given number of times.
634
635 Parameters
636 ----------
637 periods : int, default 1
638 Number of periods (or increments) to shift by,
639 can be positive or negative.
640 freq : pandas.DateOffset, pandas.Timedelta or string, optional
641 Frequency increment to shift by.
642 If None, the index is shifted by its own `freq` attribute.
643 Offset aliases are valid strings, e.g., 'D', 'W', 'M' etc.
644
645 Returns
646 -------
647 pandas.DatetimeIndex
648 Shifted index.
649
650 See Also
651 --------
652 Index.shift : Shift values of Index.
653 PeriodIndex.shift : Shift values of PeriodIndex.
654 """
655 if freq is not None and freq != self.freq:
656 if isinstance(freq, str):
657 freq = to_offset(freq)
658 offset = periods * freq
659 return self + offset
660
661 if periods == 0 or len(self) == 0:
662 # GH#14811 empty case
663 return self.copy()
664
665 if self.freq is None:
666 raise NullFrequencyError("Cannot shift with no freq")
667
668 start = self[0] + periods * self.freq
669 end = self[-1] + periods * self.freq
670
671 # Note: in the DatetimeTZ case, _generate_range will infer the
672 # appropriate timezone from `start` and `end`, so tz does not need
673 # to be passed explicitly.
674 result = self._data._generate_range(
675 start=start, end=end, periods=None, freq=self.freq, unit=self.unit
676 )
677 return type(self)._simple_new(result, name=self.name)
678
679 @cache_readonly
680 def inferred_freq(self) -> str | None:
681 """
682 Return the inferred frequency of the index.
683
684 Returns
685 -------
686 str or None
687 A string representing a frequency generated by ``infer_freq``.
688 Returns ``None`` if the frequency cannot be inferred.
689
690 See Also
691 --------
692 DatetimeIndex.freqstr : Return the frequency object as a string if it's set,
693 otherwise ``None``.
694
695 Examples
696 --------
697 For ``DatetimeIndex``:
698
699 >>> idx = pd.DatetimeIndex(["2018-01-01", "2018-01-03", "2018-01-05"])
700 >>> idx.inferred_freq
701 '2D'
702
703 For ``TimedeltaIndex``:
704
705 >>> tdelta_idx = pd.to_timedelta(["0 days", "10 days", "20 days"])
706 >>> tdelta_idx
707 TimedeltaIndex(['0 days', '10 days', '20 days'],
708 dtype='timedelta64[us]', freq=None)
709 >>> tdelta_idx.inferred_freq
710 '10D'
711 """
712 return self._data.inferred_freq
713
714 # --------------------------------------------------------------------
715 # Set Operation Methods
716
717 @cache_readonly
718 def _as_range_index(self) -> RangeIndex:
719 # Convert our i8 representations to RangeIndex
720 # Caller is responsible for checking isinstance(self.freq, Tick)
721 freq = cast(Tick, self.freq)
722 tick = Timedelta(freq).as_unit(self.unit)._value
723 rng = range(self[0]._value, self[-1]._value + tick, tick)
724 return RangeIndex(rng)
725
726 def _can_range_setop(self, other) -> bool:
727 return isinstance(self.freq, Tick) and isinstance(other.freq, Tick)
728
729 def _wrap_range_setop(self, other, res_i8) -> Self:
730 new_freq = None
731 if not len(res_i8):
732 # RangeIndex defaults to step=1, which we don't want.
733 new_freq = self.freq
734 elif isinstance(res_i8, RangeIndex):
735 new_freq = to_offset(
736 Timedelta(res_i8.step, unit=self.unit).as_unit(self.unit)
737 )
738
739 # TODO(GH#41493): we cannot just do
740 # type(self._data)(res_i8.values, dtype=self.dtype, freq=new_freq)
741 # because test_setops_preserve_freq fails with _validate_frequency raising.
742 # This raising is incorrect, as 'on_freq' is incorrect. This will
743 # be fixed by GH#41493
744 res_values = res_i8.values.view(self._data._ndarray.dtype)
745 result = type(self._data)._simple_new(
746 # error: Argument "dtype" to "_simple_new" of "DatetimeArray" has
747 # incompatible type "Union[dtype[Any], ExtensionDtype]"; expected
748 # "Union[dtype[datetime64], DatetimeTZDtype]"
749 res_values,
750 dtype=self.dtype, # type: ignore[arg-type]
751 freq=new_freq, # type: ignore[arg-type]
752 )
753 return cast("Self", self._wrap_setop_result(other, result))
754
755 def _range_intersect(self, other, sort) -> Self:
756 # Dispatch to RangeIndex intersection logic.
757 left = self._as_range_index
758 right = other._as_range_index
759 res_i8 = left.intersection(right, sort=sort)
760 return self._wrap_range_setop(other, res_i8)
761
762 def _range_union(self, other, sort) -> Self:
763 # Dispatch to RangeIndex union logic.
764 left = self._as_range_index
765 right = other._as_range_index
766 res_i8 = left.union(right, sort=sort)
767 return self._wrap_range_setop(other, res_i8)
768
769 def _intersection(self, other: Index, sort: bool = False) -> Index:
770 """
771 intersection specialized to the case with matching dtypes and both non-empty.
772 """
773 other = cast("DatetimeTimedeltaMixin", other)
774
775 if self._can_range_setop(other):
776 return self._range_intersect(other, sort=sort)
777
778 if not self._can_fast_intersect(other):
779 result = Index._intersection(self, other, sort=sort)
780 # We need to invalidate the freq because Index._intersection
781 # uses _shallow_copy on a view of self._data, which will preserve
782 # self.freq if we're not careful.
783 # At this point we should have result.dtype == self.dtype
784 # and type(result) is type(self._data)
785 result = self._wrap_setop_result(other, result)
786 # error: "Index" has no attribute "_with_freq"; maybe "_with_infer"?
787 return result._with_freq(None)._with_freq("infer") # type: ignore[attr-defined]
788
789 else:
790 return self._fast_intersect(other, sort)
791
792 def _fast_intersect(self, other, sort):
793 # to make our life easier, "sort" the two ranges
794 if self[0] <= other[0]:
795 left, right = self, other
796 else:
797 left, right = other, self
798
799 # after sorting, the intersection always starts with the right index
800 # and ends with the index of which the last elements is smallest
801 end = min(left[-1], right[-1])
802 start = right[0]
803
804 if end < start:
805 result = self[:0]
806 else:
807 lslice = slice(*left.slice_locs(start, end))
808 result = left._values[lslice]
809
810 return result
811
812 def _can_fast_intersect(self, other: Self) -> bool:
813 # Note: we only get here with len(self) > 0 and len(other) > 0
814 if self.freq is None:
815 return False
816
817 elif other.freq != self.freq:
818 return False
819
820 elif not self.is_monotonic_increasing:
821 # Because freq is not None, we must then be monotonic decreasing
822 return False
823
824 # this along with matching freqs ensure that we "line up",
825 # so intersection will preserve freq
826 # Note we are assuming away Ticks, as those go through _range_intersect
827 # GH#42104
828 return self.freq.n == 1
829
830 def _can_fast_union(self, other: Self) -> bool:
831 # Assumes that type(self) == type(other), as per the annotation
832 # The ability to fast_union also implies that `freq` should be
833 # retained on union.
834 freq = self.freq
835
836 if freq is None or freq != other.freq:
837 return False
838
839 if not self.is_monotonic_increasing:
840 # Because freq is not None, we must then be monotonic decreasing
841 # TODO: do union on the reversed indexes?
842 return False
843
844 if len(self) == 0 or len(other) == 0:
845 # only reached via union_many
846 return True
847
848 # to make our life easier, "sort" the two ranges
849 if self[0] <= other[0]:
850 left, right = self, other
851 else:
852 left, right = other, self
853
854 right_start = right[0]
855 left_end = left[-1]
856
857 # Only need to "adjoin", not overlap
858 return (right_start == left_end + freq) or right_start in left
859
860 def _fast_union(self, other: Self, sort=None) -> Self:
861 # Caller is responsible for ensuring self and other are non-empty
862
863 # to make our life easier, "sort" the two ranges
864 if self[0] <= other[0]:
865 left, right = self, other
866 elif sort is False:
867 # TDIs are not in the "correct" order and we don't want
868 # to sort but want to remove overlaps
869 left, right = self, other
870 left_start = left[0]
871 loc = right.searchsorted(left_start, side="left")
872 right_chunk = right._values[:loc]
873 dates = concat_compat((left._values, right_chunk))
874 result = type(self)._simple_new(dates, name=self.name)
875 return result
876 else:
877 left, right = other, self
878
879 left_end = left[-1]
880 right_end = right[-1]
881
882 # concatenate
883 if left_end < right_end:
884 loc = right.searchsorted(left_end, side="right")
885 right_chunk = right._values[loc:]
886 dates = concat_compat([left._values, right_chunk])
887 # The can_fast_union check ensures that the result.freq
888 # should match self.freq
889 assert isinstance(dates, type(self._data))
890 # error: Item "ExtensionArray" of "ExtensionArray |
891 # ndarray[Any, Any]" has no attribute "_freq"
892 assert dates._freq == self.freq # type: ignore[union-attr]
893 result = type(self)._simple_new(dates)
894 return result
895 else:
896 return left
897
898 def _union(self, other, sort):
899 # We are called by `union`, which is responsible for this validation
900 assert isinstance(other, type(self))
901 assert self.dtype == other.dtype
902
903 if self._can_range_setop(other):
904 return self._range_union(other, sort=sort)
905
906 if self._can_fast_union(other):
907 result = self._fast_union(other, sort=sort)
908 # in the case with sort=None, the _can_fast_union check ensures
909 # that result.freq == self.freq
910 return result
911 else:
912 return super()._union(other, sort)._with_freq("infer")
913
914 # --------------------------------------------------------------------
915 # Join Methods
916
917 def _get_join_freq(self, other):
918 """
919 Get the freq to attach to the result of a join operation.
920 """
921 freq = None
922 if self._can_fast_union(other):
923 freq = self.freq
924 return freq
925
926 def _wrap_join_result(
927 self,
928 joined,
929 other,
930 lidx: npt.NDArray[np.intp] | None,
931 ridx: npt.NDArray[np.intp] | None,
932 how: JoinHow,
933 ) -> tuple[Self, npt.NDArray[np.intp] | None, npt.NDArray[np.intp] | None]:
934 assert other.dtype == self.dtype, (other.dtype, self.dtype)
935 join_index, lidx, ridx = super()._wrap_join_result(
936 joined, other, lidx, ridx, how
937 )
938 join_index._data._freq = self._get_join_freq(other)
939 return join_index, lidx, ridx
940
941 def _get_engine_target(self) -> np.ndarray:
942 # engine methods and libjoin methods need dt64/td64 values cast to i8
943 return self._data._ndarray.view("i8")
944
945 def _from_join_target(self, result: np.ndarray):
946 # view e.g. i8 back to M8[ns]
947 result = result.view(self._data._ndarray.dtype)
948 return self._data._from_backing_data(result)
949
950 def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
951 if (
952 self.is_monotonic_increasing
953 and isinstance(label, (Timestamp, Timedelta))
954 and abbrev_to_npy_unit(label.unit) > abbrev_to_npy_unit(self.unit)
955 ):
956 # For non-matching units we can safely round down (with side=right)
957 # This is needed for GH#63262
958 if side == "right":
959 label = label.as_unit(self.unit) # this should always be a round-down
960 else:
961 # round up
962 label = label.ceil(self.unit).as_unit(self.unit)
963
964 return super()._searchsorted_monotonic(label, side)
965
966 # --------------------------------------------------------------------
967 # List-like Methods
968
969 def _get_delete_freq(self, loc: int | slice | Sequence[int]):
970 """
971 Find the `freq` for self.delete(loc).
972 """
973 freq = None
974 if self.freq is not None:
975 if is_integer(loc):
976 if loc in (0, -len(self), -1, len(self) - 1):
977 freq = self.freq
978 else:
979 if is_list_like(loc):
980 # error: Incompatible types in assignment (expression has
981 # type "Union[slice, ndarray]", variable has type
982 # "Union[int, slice, Sequence[int]]")
983 loc = lib.maybe_indices_to_slice( # type: ignore[assignment]
984 np.asarray(loc, dtype=np.intp), len(self)
985 )
986 if isinstance(loc, slice) and loc.step in (1, None):
987 if loc.start in (0, None) or loc.stop in (len(self), None):
988 freq = self.freq
989 return freq
990
991 def _get_insert_freq(self, loc: int, item):
992 """
993 Find the `freq` for self.insert(loc, item).
994 """
995 value = self._data._validate_scalar(item)
996 item = self._data._box_func(value)
997
998 freq = None
999 if self.freq is not None:
1000 # freq can be preserved on edge cases
1001 if self.size:
1002 if item is NaT:
1003 pass
1004 elif loc in (0, -len(self)) and item + self.freq == self[0]:
1005 freq = self.freq
1006 elif (loc == len(self)) and item - self.freq == self[-1]:
1007 freq = self.freq
1008 # Adding a single item to an empty index may preserve freq
1009 elif isinstance(self.freq, Tick):
1010 # all TimedeltaIndex cases go through here; is_on_offset
1011 # would raise TypeError
1012 freq = self.freq
1013 elif self.freq.is_on_offset(item):
1014 freq = self.freq
1015 return freq
1016
1017 def delete(self, loc) -> Self:
1018 """
1019 Make new Index with passed location(-s) deleted.
1020
1021 Parameters
1022 ----------
1023 loc : int or list of int
1024 Location of item(-s) which will be deleted.
1025 Use a list of locations to delete more than one value at the same time.
1026
1027 Returns
1028 -------
1029 Index
1030 Will be same type as self, except for RangeIndex.
1031
1032 See Also
1033 --------
1034 numpy.delete : Delete any rows and column from NumPy array (ndarray).
1035
1036 Examples
1037 --------
1038 >>> idx = pd.Index(["a", "b", "c"])
1039 >>> idx.delete(1)
1040 Index(['a', 'c'], dtype='str')
1041 >>> idx = pd.Index(["a", "b", "c"])
1042 >>> idx.delete([0, 2])
1043 Index(['b'], dtype='str')
1044 """
1045 result = super().delete(loc)
1046 result._data._freq = self._get_delete_freq(loc)
1047 return result
1048
1049 def insert(self, loc: int, item):
1050 """
1051 Make new Index inserting new item at location.
1052 Follows Python numpy.insert semantics for negative values.
1053
1054 Parameters
1055 ----------
1056 loc : int
1057 The integer location where the new item will be inserted.
1058 item : object
1059 The new item to be inserted into the Index.
1060
1061 Returns
1062 -------
1063 Index
1064 Returns a new Index object resulting from inserting the specified item at
1065 the specified location within the original Index.
1066
1067 See Also
1068 --------
1069 Index.append : Append a collection of Indexes together.
1070
1071 Examples
1072 --------
1073 >>> idx = pd.Index(["a", "b", "c"])
1074 >>> idx.insert(1, "x")
1075 Index(['a', 'x', 'b', 'c'], dtype='str')
1076 """
1077 result = super().insert(loc, item)
1078 if isinstance(result, type(self)):
1079 # i.e. parent class method did not cast
1080 result._data._freq = self._get_insert_freq(loc, item)
1081 return result
1082
1083 # --------------------------------------------------------------------
1084 # NDArray-Like Methods
1085
1086 def take(
1087 self,
1088 indices,
1089 axis: Axis = 0,
1090 allow_fill: bool = True,
1091 fill_value=None,
1092 **kwargs,
1093 ) -> Self:
1094 """
1095 Return a new Index of the values selected by the indices.
1096 For internal compatibility with numpy arrays.
1097
1098 Parameters
1099 ----------
1100 indices : array-like
1101 Indices to be taken.
1102 axis : {0 or 'index'}, optional
1103 The axis over which to select values, always 0 or 'index'.
1104 allow_fill : bool, default True
1105 How to handle negative values in `indices`.
1106 * False: negative values in `indices` indicate positional indices
1107 from the right (the default). This is similar to
1108 :func:`numpy.take`.
1109 * True: negative values in `indices` indicate
1110 missing values. These values are set to `fill_value`. Any other
1111 other negative values raise a ``ValueError``.
1112 fill_value : scalar, default None
1113 If allow_fill=True and fill_value is not None, indices specified by
1114 -1 are regarded as NA. If Index doesn't hold NA, raise ValueError.
1115 **kwargs
1116 Required for compatibility with numpy.
1117
1118 Returns
1119 -------
1120 Index
1121 An index formed of elements at the given indices. Will be the same
1122 type as self, except for RangeIndex.
1123
1124 See Also
1125 --------
1126 numpy.ndarray.take: Return an array formed from the
1127 elements of a at the given indices.
1128
1129 Examples
1130 --------
1131 >>> idx = pd.Index(["a", "b", "c"])
1132 >>> idx.take([2, 2, 1, 2])
1133 Index(['c', 'c', 'b', 'c'], dtype='str')
1134 """
1135 nv.validate_take((), kwargs)
1136 indices = np.asarray(indices, dtype=np.intp)
1137
1138 result = NDArrayBackedExtensionIndex.take(
1139 self, indices, axis, allow_fill, fill_value, **kwargs
1140 )
1141
1142 maybe_slice = lib.maybe_indices_to_slice(indices, len(self))
1143 if isinstance(maybe_slice, slice):
1144 freq = self._data._get_getitem_freq(maybe_slice)
1145 result._data._freq = freq
1146 return result