Skip to content

Commit a1a2f66

Browse files
jbrockmendeljreback
authored andcommitted
REF: move range-generation functions to EA mixin classes (#22016)
1 parent 7110a34 commit a1a2f66

File tree

9 files changed

+573
-455
lines changed

9 files changed

+573
-455
lines changed

pandas/core/arrays/datetimelike.py

+228-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# -*- coding: utf-8 -*-
2+
from datetime import datetime, timedelta
23
import operator
34
import warnings
45

@@ -8,7 +9,7 @@
89
from pandas._libs.tslibs import timezones
910
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds, Timedelta
1011
from pandas._libs.tslibs.period import (
11-
DIFFERENT_FREQ_INDEX, IncompatibleFrequency)
12+
Period, DIFFERENT_FREQ_INDEX, IncompatibleFrequency)
1213

1314
from pandas.errors import NullFrequencyError, PerformanceWarning
1415
from pandas import compat
@@ -19,6 +20,13 @@
1920
from pandas.core.dtypes.common import (
2021
needs_i8_conversion,
2122
is_list_like,
23+
is_offsetlike,
24+
is_extension_array_dtype,
25+
is_datetime64_dtype,
26+
is_datetime64_any_dtype,
27+
is_datetime64tz_dtype,
28+
is_float_dtype,
29+
is_integer_dtype,
2230
is_bool_dtype,
2331
is_period_dtype,
2432
is_timedelta64_dtype,
@@ -100,7 +108,7 @@ class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin):
100108
_freq
101109
102110
and that the inheriting class has methods:
103-
_validate_frequency
111+
_generate_range
104112
"""
105113

106114
@property
@@ -132,6 +140,14 @@ def asi8(self):
132140
# ------------------------------------------------------------------
133141
# Array-like Methods
134142

143+
@property
144+
def shape(self):
145+
return (len(self),)
146+
147+
@property
148+
def size(self):
149+
return np.prod(self.shape)
150+
135151
def __len__(self):
136152
return len(self._data)
137153

@@ -296,6 +312,34 @@ def resolution(self):
296312
"""
297313
return frequencies.Resolution.get_str(self._resolution)
298314

315+
@classmethod
316+
def _validate_frequency(cls, index, freq, **kwargs):
317+
"""
318+
Validate that a frequency is compatible with the values of a given
319+
Datetime Array/Index or Timedelta Array/Index
320+
321+
Parameters
322+
----------
323+
index : DatetimeIndex or TimedeltaIndex
324+
The index on which to determine if the given frequency is valid
325+
freq : DateOffset
326+
The frequency to validate
327+
"""
328+
if is_period_dtype(cls):
329+
# Frequency validation is not meaningful for Period Array/Index
330+
return None
331+
332+
inferred = index.inferred_freq
333+
if index.size == 0 or inferred == freq.freqstr:
334+
return None
335+
336+
on_freq = cls._generate_range(start=index[0], end=None,
337+
periods=len(index), freq=freq, **kwargs)
338+
if not np.array_equal(index.asi8, on_freq.asi8):
339+
raise ValueError('Inferred frequency {infer} from passed values '
340+
'does not conform to passed frequency {passed}'
341+
.format(infer=inferred, passed=freq.freqstr))
342+
299343
# ------------------------------------------------------------------
300344
# Arithmetic Methods
301345

@@ -477,6 +521,188 @@ def _addsub_offset_array(self, other, op):
477521
kwargs['freq'] = 'infer'
478522
return type(self)(res_values, **kwargs)
479523

524+
def shift(self, n, freq=None):
525+
"""
526+
Specialized shift which produces a Datetime/Timedelta Array/Index
527+
528+
Parameters
529+
----------
530+
n : int
531+
Periods to shift by
532+
freq : DateOffset or timedelta-like, optional
533+
534+
Returns
535+
-------
536+
shifted : same type as self
537+
"""
538+
if freq is not None and freq != self.freq:
539+
if isinstance(freq, compat.string_types):
540+
freq = frequencies.to_offset(freq)
541+
offset = n * freq
542+
result = self + offset
543+
544+
if hasattr(self, 'tz'):
545+
result._tz = self.tz
546+
547+
return result
548+
549+
if n == 0:
550+
# immutable so OK
551+
return self
552+
553+
if self.freq is None:
554+
raise NullFrequencyError("Cannot shift with no freq")
555+
556+
start = self[0] + n * self.freq
557+
end = self[-1] + n * self.freq
558+
attribs = self._get_attributes_dict()
559+
return self._generate_range(start=start, end=end, periods=None,
560+
**attribs)
561+
562+
@classmethod
563+
def _add_datetimelike_methods(cls):
564+
"""
565+
add in the datetimelike methods (as we may have to override the
566+
superclass)
567+
"""
568+
569+
def __add__(self, other):
570+
other = lib.item_from_zerodim(other)
571+
if isinstance(other, (ABCSeries, ABCDataFrame)):
572+
return NotImplemented
573+
574+
# scalar others
575+
elif other is NaT:
576+
result = self._add_nat()
577+
elif isinstance(other, (Tick, timedelta, np.timedelta64)):
578+
result = self._add_delta(other)
579+
elif isinstance(other, DateOffset):
580+
# specifically _not_ a Tick
581+
result = self._add_offset(other)
582+
elif isinstance(other, (datetime, np.datetime64)):
583+
result = self._add_datelike(other)
584+
elif lib.is_integer(other):
585+
# This check must come after the check for np.timedelta64
586+
# as is_integer returns True for these
587+
result = self.shift(other)
588+
589+
# array-like others
590+
elif is_timedelta64_dtype(other):
591+
# TimedeltaIndex, ndarray[timedelta64]
592+
result = self._add_delta(other)
593+
elif is_offsetlike(other):
594+
# Array/Index of DateOffset objects
595+
result = self._addsub_offset_array(other, operator.add)
596+
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
597+
# DatetimeIndex, ndarray[datetime64]
598+
return self._add_datelike(other)
599+
elif is_integer_dtype(other):
600+
result = self._addsub_int_array(other, operator.add)
601+
elif is_float_dtype(other) or is_period_dtype(other):
602+
# Explicitly catch invalid dtypes
603+
raise TypeError("cannot add {dtype}-dtype to {cls}"
604+
.format(dtype=other.dtype,
605+
cls=type(self).__name__))
606+
elif is_extension_array_dtype(other):
607+
# Categorical op will raise; defer explicitly
608+
return NotImplemented
609+
else: # pragma: no cover
610+
return NotImplemented
611+
612+
return result
613+
614+
cls.__add__ = __add__
615+
616+
def __radd__(self, other):
617+
# alias for __add__
618+
return self.__add__(other)
619+
cls.__radd__ = __radd__
620+
621+
def __sub__(self, other):
622+
other = lib.item_from_zerodim(other)
623+
if isinstance(other, (ABCSeries, ABCDataFrame)):
624+
return NotImplemented
625+
626+
# scalar others
627+
elif other is NaT:
628+
result = self._sub_nat()
629+
elif isinstance(other, (Tick, timedelta, np.timedelta64)):
630+
result = self._add_delta(-other)
631+
elif isinstance(other, DateOffset):
632+
# specifically _not_ a Tick
633+
result = self._add_offset(-other)
634+
elif isinstance(other, (datetime, np.datetime64)):
635+
result = self._sub_datelike(other)
636+
elif lib.is_integer(other):
637+
# This check must come after the check for np.timedelta64
638+
# as is_integer returns True for these
639+
result = self.shift(-other)
640+
elif isinstance(other, Period):
641+
result = self._sub_period(other)
642+
643+
# array-like others
644+
elif is_timedelta64_dtype(other):
645+
# TimedeltaIndex, ndarray[timedelta64]
646+
result = self._add_delta(-other)
647+
elif is_offsetlike(other):
648+
# Array/Index of DateOffset objects
649+
result = self._addsub_offset_array(other, operator.sub)
650+
elif is_datetime64_dtype(other) or is_datetime64tz_dtype(other):
651+
# DatetimeIndex, ndarray[datetime64]
652+
result = self._sub_datelike(other)
653+
elif is_period_dtype(other):
654+
# PeriodIndex
655+
result = self._sub_period_array(other)
656+
elif is_integer_dtype(other):
657+
result = self._addsub_int_array(other, operator.sub)
658+
elif isinstance(other, ABCIndexClass):
659+
raise TypeError("cannot subtract {cls} and {typ}"
660+
.format(cls=type(self).__name__,
661+
typ=type(other).__name__))
662+
elif is_float_dtype(other):
663+
# Explicitly catch invalid dtypes
664+
raise TypeError("cannot subtract {dtype}-dtype from {cls}"
665+
.format(dtype=other.dtype,
666+
cls=type(self).__name__))
667+
elif is_extension_array_dtype(other):
668+
# Categorical op will raise; defer explicitly
669+
return NotImplemented
670+
else: # pragma: no cover
671+
return NotImplemented
672+
673+
return result
674+
675+
cls.__sub__ = __sub__
676+
677+
def __rsub__(self, other):
678+
if is_datetime64_dtype(other) and is_timedelta64_dtype(self):
679+
# ndarray[datetime64] cannot be subtracted from self, so
680+
# we need to wrap in DatetimeArray/Index and flip the operation
681+
if not isinstance(other, DatetimeLikeArrayMixin):
682+
# Avoid down-casting DatetimeIndex
683+
from pandas.core.arrays import DatetimeArrayMixin
684+
other = DatetimeArrayMixin(other)
685+
return other - self
686+
elif (is_datetime64_any_dtype(self) and hasattr(other, 'dtype') and
687+
not is_datetime64_any_dtype(other)):
688+
# GH#19959 datetime - datetime is well-defined as timedelta,
689+
# but any other type - datetime is not well-defined.
690+
raise TypeError("cannot subtract {cls} from {typ}"
691+
.format(cls=type(self).__name__,
692+
typ=type(other).__name__))
693+
return -(self - other)
694+
cls.__rsub__ = __rsub__
695+
696+
def __iadd__(self, other):
697+
# alias for __add__
698+
return self.__add__(other)
699+
cls.__iadd__ = __iadd__
700+
701+
def __isub__(self, other):
702+
# alias for __sub__
703+
return self.__sub__(other)
704+
cls.__isub__ = __isub__
705+
480706
# --------------------------------------------------------------
481707
# Comparison Methods
482708

0 commit comments

Comments
 (0)