diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 3bf6dce00a031..868f0553816d0 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -35,7 +35,7 @@ from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.tools.timedeltas import to_timedelta -from pandas.tseries.frequencies import to_offset +from pandas.tseries.frequencies import DateOffset, to_offset _index_doc_kwargs = dict(ibase._index_doc_kwargs) @@ -71,6 +71,36 @@ def method(self, other): return method +class DatetimeTimedeltaMixin: + """ + Mixin class for methods shared by DatetimeIndex and TimedeltaIndex, + but not PeriodIndex + """ + + def _set_freq(self, freq): + """ + Set the _freq attribute on our underlying DatetimeArray. + + Parameters + ---------- + freq : DateOffset, None, or "infer" + """ + # GH#29843 + if freq is None: + # Always valid + pass + elif len(self) == 0 and isinstance(freq, DateOffset): + # Always valid. In the TimedeltaIndex case, we assume this + # is a Tick offset. + pass + else: + # As an internal method, we can ensure this assertion always holds + assert freq == "infer" + freq = to_offset(self.inferred_freq) + + self._data._freq = freq + + class DatetimeIndexOpsMixin(ExtensionOpsMixin): """ Common ops mixin to support a unified interface datetimelike Index. @@ -592,8 +622,7 @@ def intersection(self, other, sort=False): result = Index.intersection(self, other, sort=sort) if isinstance(result, type(self)): if result.freq is None: - # TODO: find a less code-smelly way to set this - result._data._freq = to_offset(result.inferred_freq) + result._set_freq("infer") return result elif ( @@ -608,8 +637,7 @@ def intersection(self, other, sort=False): # Invalidate the freq of `result`, which may not be correct at # this point, depending on the values. - # TODO: find a less code-smelly way to set this - result._data._freq = None + result._set_freq(None) if hasattr(self, "tz"): result = self._shallow_copy( result._values, name=result.name, tz=result.tz, freq=None @@ -617,8 +645,7 @@ def intersection(self, other, sort=False): else: result = self._shallow_copy(result._values, name=result.name, freq=None) if result.freq is None: - # TODO: find a less code-smelly way to set this - result._data._freq = to_offset(result.inferred_freq) + result._set_freq("infer") return result # to make our life easier, "sort" the two ranges diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 1fd962dd24656..c81d1076f1015 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -34,6 +34,7 @@ from pandas.core.indexes.datetimelike import ( DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, + DatetimeTimedeltaMixin, ea_passthrough, ) from pandas.core.indexes.numeric import Int64Index @@ -93,7 +94,9 @@ class DatetimeDelegateMixin(DatetimelikeDelegateMixin): typ="method", overwrite=False, ) -class DatetimeIndex(DatetimeIndexOpsMixin, Int64Index, DatetimeDelegateMixin): +class DatetimeIndex( + DatetimeTimedeltaMixin, DatetimeIndexOpsMixin, Int64Index, DatetimeDelegateMixin +): """ Immutable ndarray of datetime64 data, represented internally as int64, and which can be boxed to Timestamp objects that are subclasses of datetime and @@ -412,7 +415,7 @@ def _convert_for_op(self, value): @Appender(Index.difference.__doc__) def difference(self, other, sort=None): new_idx = super().difference(other, sort=sort) - new_idx._data._freq = None + new_idx._set_freq(None) return new_idx # -------------------------------------------------------------------- diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 889075ebe4e31..23a42b7173c2c 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -29,6 +29,7 @@ from pandas.core.indexes.datetimelike import ( DatetimeIndexOpsMixin, DatetimelikeDelegateMixin, + DatetimeTimedeltaMixin, ea_passthrough, ) from pandas.core.indexes.numeric import Int64Index @@ -64,7 +65,11 @@ class TimedeltaDelegateMixin(DatetimelikeDelegateMixin): overwrite=True, ) class TimedeltaIndex( - DatetimeIndexOpsMixin, dtl.TimelikeOps, Int64Index, TimedeltaDelegateMixin + DatetimeTimedeltaMixin, + DatetimeIndexOpsMixin, + dtl.TimelikeOps, + Int64Index, + TimedeltaDelegateMixin, ): """ Immutable ndarray of timedelta64 data, represented internally as int64, and @@ -296,8 +301,7 @@ def _union(self, other, sort): result = Index._union(this, other, sort=sort) if isinstance(result, TimedeltaIndex): if result.freq is None: - # TODO: find a less code-smelly way to set this - result._data._freq = to_offset(result.inferred_freq) + result._set_freq("infer") return result def join(self, other, how="left", level=None, return_indexers=False, sort=False): @@ -350,8 +354,7 @@ def intersection(self, other, sort=False): @Appender(Index.difference.__doc__) def difference(self, other, sort=None): new_idx = super().difference(other, sort=sort) - # TODO: find a less code-smelly way to set this - new_idx._data._freq = None + new_idx._set_freq(None) return new_idx def _wrap_joined_index(self, joined, other): diff --git a/pandas/core/resample.py b/pandas/core/resample.py index 2294c846e81c7..bcac5c4d2913b 100644 --- a/pandas/core/resample.py +++ b/pandas/core/resample.py @@ -1025,8 +1025,7 @@ def _downsample(self, how, **kwargs): if not len(ax): # reset to the new freq obj = obj.copy() - # TODO: find a less code-smelly way to set this - obj.index._data._freq = self.freq + obj.index._set_freq(self.freq) return obj # do we have a regular frequency