Skip to content

Commit 7827a0c

Browse files
jbrockmendelhweecat
authored andcommitted
REF: share code between DatetimeIndex and TimedeltaIndex (pandas-dev#30587)
1 parent e27bbea commit 7827a0c

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))
@@ -778,6 +693,168 @@ def shift(self, periods, freq=None):
778693
return type(self)(result, name=self.name)
779694

780695

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

0 commit comments

Comments
 (0)