Skip to content

Commit 11664c3

Browse files
jbrockmendeljreback
authored andcommitted
REF: share code between DatetimeIndex and TimedeltaIndex (#30587)
1 parent d788234 commit 11664c3

File tree

4 files changed

+172
-252
lines changed

4 files changed

+172
-252
lines changed

pandas/core/indexes/datetimelike.py

Lines changed: 167 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
)
3434
import pandas.core.indexes.base as ibase
3535
from pandas.core.indexes.base import Index, _index_shared_docs
36+
from pandas.core.indexes.numeric import Int64Index
3637
from pandas.core.tools.timedeltas import to_timedelta
3738

3839
from pandas.tseries.frequencies import DateOffset, to_offset
@@ -71,36 +72,6 @@ def method(self, other):
7172
return method
7273

7374

74-
class DatetimeTimedeltaMixin:
75-
"""
76-
Mixin class for methods shared by DatetimeIndex and TimedeltaIndex,
77-
but not PeriodIndex
78-
"""
79-
80-
def _set_freq(self, freq):
81-
"""
82-
Set the _freq attribute on our underlying DatetimeArray.
83-
84-
Parameters
85-
----------
86-
freq : DateOffset, None, or "infer"
87-
"""
88-
# GH#29843
89-
if freq is None:
90-
# Always valid
91-
pass
92-
elif len(self) == 0 and isinstance(freq, DateOffset):
93-
# Always valid. In the TimedeltaIndex case, we assume this
94-
# is a Tick offset.
95-
pass
96-
else:
97-
# As an internal method, we can ensure this assertion always holds
98-
assert freq == "infer"
99-
freq = to_offset(self.inferred_freq)
100-
101-
self._data._freq = freq
102-
103-
10475
class DatetimeIndexOpsMixin(ExtensionOpsMixin):
10576
"""
10677
Common ops mixin to support a unified interface datetimelike Index.
@@ -125,6 +96,10 @@ class DatetimeIndexOpsMixin(ExtensionOpsMixin):
12596
__iter__ = ea_passthrough(DatetimeLikeArrayMixin.__iter__)
12697
mean = ea_passthrough(DatetimeLikeArrayMixin.mean)
12798

99+
@property
100+
def is_all_dates(self) -> bool:
101+
return True
102+
128103
@property
129104
def freq(self):
130105
"""
@@ -605,66 +580,6 @@ def isin(self, values, level=None):
605580

606581
return algorithms.isin(self.asi8, values.asi8)
607582

608-
def intersection(self, other, sort=False):
609-
self._validate_sort_keyword(sort)
610-
self._assert_can_do_setop(other)
611-
612-
if self.equals(other):
613-
return self._get_reconciled_name_object(other)
614-
615-
if len(self) == 0:
616-
return self.copy()
617-
if len(other) == 0:
618-
return other.copy()
619-
620-
if not isinstance(other, type(self)):
621-
result = Index.intersection(self, other, sort=sort)
622-
if isinstance(result, type(self)):
623-
if result.freq is None:
624-
result._set_freq("infer")
625-
return result
626-
627-
elif (
628-
other.freq is None
629-
or self.freq is None
630-
or other.freq != self.freq
631-
or not other.freq.is_anchored()
632-
or (not self.is_monotonic or not other.is_monotonic)
633-
):
634-
result = Index.intersection(self, other, sort=sort)
635-
636-
# Invalidate the freq of `result`, which may not be correct at
637-
# this point, depending on the values.
638-
639-
result._set_freq(None)
640-
if hasattr(self, "tz"):
641-
result = self._shallow_copy(
642-
result._values, name=result.name, tz=result.tz, freq=None
643-
)
644-
else:
645-
result = self._shallow_copy(result._values, name=result.name, freq=None)
646-
if result.freq is None:
647-
result._set_freq("infer")
648-
return result
649-
650-
# to make our life easier, "sort" the two ranges
651-
if self[0] <= other[0]:
652-
left, right = self, other
653-
else:
654-
left, right = other, self
655-
656-
# after sorting, the intersection always starts with the right index
657-
# and ends with the index of which the last elements is smallest
658-
end = min(left[-1], right[-1])
659-
start = right[0]
660-
661-
if end < start:
662-
return type(self)(data=[])
663-
else:
664-
lslice = slice(*left.slice_locs(start, end))
665-
left_chunk = left.values[lslice]
666-
return self._shallow_copy(left_chunk)
667-
668583
@Appender(_index_shared_docs["repeat"] % _index_doc_kwargs)
669584
def repeat(self, repeats, axis=None):
670585
nv.validate_repeat(tuple(), dict(axis=axis))
@@ -777,6 +692,168 @@ def shift(self, periods=1, freq=None):
777692
return type(self)(result, name=self.name)
778693

779694

695+
class DatetimeTimedeltaMixin(DatetimeIndexOpsMixin, Int64Index):
696+
"""
697+
Mixin class for methods shared by DatetimeIndex and TimedeltaIndex,
698+
but not PeriodIndex
699+
"""
700+
701+
# Compat for frequency inference, see GH#23789
702+
_is_monotonic_increasing = Index.is_monotonic_increasing
703+
_is_monotonic_decreasing = Index.is_monotonic_decreasing
704+
_is_unique = Index.is_unique
705+
706+
def _set_freq(self, freq):
707+
"""
708+
Set the _freq attribute on our underlying DatetimeArray.
709+
710+
Parameters
711+
----------
712+
freq : DateOffset, None, or "infer"
713+
"""
714+
# GH#29843
715+
if freq is None:
716+
# Always valid
717+
pass
718+
elif len(self) == 0 and isinstance(freq, DateOffset):
719+
# Always valid. In the TimedeltaIndex case, we assume this
720+
# is a Tick offset.
721+
pass
722+
else:
723+
# As an internal method, we can ensure this assertion always holds
724+
assert freq == "infer"
725+
freq = to_offset(self.inferred_freq)
726+
727+
self._data._freq = freq
728+
729+
# --------------------------------------------------------------------
730+
# Set Operation Methods
731+
732+
@Appender(Index.difference.__doc__)
733+
def difference(self, other, sort=None):
734+
new_idx = super().difference(other, sort=sort)
735+
new_idx._set_freq(None)
736+
return new_idx
737+
738+
def intersection(self, other, sort=False):
739+
"""
740+
Specialized intersection for DatetimeIndex/TimedeltaIndex.
741+
742+
May be much faster than Index.intersection
743+
744+
Parameters
745+
----------
746+
other : Same type as self or array-like
747+
sort : False or None, default False
748+
Sort the resulting index if possible.
749+
750+
.. versionadded:: 0.24.0
751+
752+
.. versionchanged:: 0.24.1
753+
754+
Changed the default to ``False`` to match the behaviour
755+
from before 0.24.0.
756+
757+
.. versionchanged:: 0.25.0
758+
759+
The `sort` keyword is added
760+
761+
Returns
762+
-------
763+
y : Index or same type as self
764+
"""
765+
self._validate_sort_keyword(sort)
766+
self._assert_can_do_setop(other)
767+
768+
if self.equals(other):
769+
return self._get_reconciled_name_object(other)
770+
771+
if len(self) == 0:
772+
return self.copy()
773+
if len(other) == 0:
774+
return other.copy()
775+
776+
if not isinstance(other, type(self)):
777+
result = Index.intersection(self, other, sort=sort)
778+
if isinstance(result, type(self)):
779+
if result.freq is None:
780+
result._set_freq("infer")
781+
return result
782+
783+
elif (
784+
other.freq is None
785+
or self.freq is None
786+
or other.freq != self.freq
787+
or not other.freq.is_anchored()
788+
or (not self.is_monotonic or not other.is_monotonic)
789+
):
790+
result = Index.intersection(self, other, sort=sort)
791+
792+
# Invalidate the freq of `result`, which may not be correct at
793+
# this point, depending on the values.
794+
795+
result._set_freq(None)
796+
if hasattr(self, "tz"):
797+
result = self._shallow_copy(
798+
result._values, name=result.name, tz=result.tz, freq=None
799+
)
800+
else:
801+
result = self._shallow_copy(result._values, name=result.name, freq=None)
802+
if result.freq is None:
803+
result._set_freq("infer")
804+
return result
805+
806+
# to make our life easier, "sort" the two ranges
807+
if self[0] <= other[0]:
808+
left, right = self, other
809+
else:
810+
left, right = other, self
811+
812+
# after sorting, the intersection always starts with the right index
813+
# and ends with the index of which the last elements is smallest
814+
end = min(left[-1], right[-1])
815+
start = right[0]
816+
817+
if end < start:
818+
return type(self)(data=[])
819+
else:
820+
lslice = slice(*left.slice_locs(start, end))
821+
left_chunk = left.values[lslice]
822+
return self._shallow_copy(left_chunk)
823+
824+
def _can_fast_union(self, other) -> bool:
825+
if not isinstance(other, type(self)):
826+
return False
827+
828+
freq = self.freq
829+
830+
if freq is None or freq != other.freq:
831+
return False
832+
833+
if not self.is_monotonic or not other.is_monotonic:
834+
return False
835+
836+
if len(self) == 0 or len(other) == 0:
837+
return True
838+
839+
# to make our life easier, "sort" the two ranges
840+
if self[0] <= other[0]:
841+
left, right = self, other
842+
else:
843+
left, right = other, self
844+
845+
right_start = right[0]
846+
left_end = left[-1]
847+
848+
# Only need to "adjoin", not overlap
849+
try:
850+
return (right_start == left_end + freq) or right_start in left
851+
except ValueError:
852+
# if we are comparing a freq that does not propagate timezones
853+
# this will raise
854+
return False
855+
856+
780857
def wrap_arithmetic_op(self, other, result):
781858
if result is NotImplemented:
782859
return NotImplemented

0 commit comments

Comments
 (0)