From df9f8940104f666de786d2570da91c99f802e613 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 25 Feb 2018 21:14:12 -0800 Subject: [PATCH 01/13] implement DatetimeLikeArray --- pandas/core/indexes/datetimelike.py | 251 ++++++++++++++++------------ pandas/core/indexes/datetimes.py | 163 +++++++++--------- pandas/core/indexes/period.py | 41 ++--- pandas/core/indexes/timedeltas.py | 24 +-- 4 files changed, 266 insertions(+), 213 deletions(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 9411428b2e68d..bcda5ac23f7ec 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -121,8 +121,149 @@ def ceil(self, freq): return self._round(freq, np.ceil) -class DatetimeIndexOpsMixin(object): +class DatetimeLikeArray(object): + """ + Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray + + Assumes that __new__/__init__ defines: + _data + freq + """ + + @property + def _box_func(self): + """ + box function to get object from internal representation + """ + raise com.AbstractMethodError(self) + + def __iter__(self): + return (self._box_func(v) for v in self.asi8) + + @property + def values(self): + """ return the underlying data as an ndarray """ + return self._data.view(np.ndarray) + + @property + def asi8(self): + # do not cache or you'll create a memory leak + return self.values.view('i8') + + # ------------------------------------------------------------------ + # Null Handling + + @property # NB: override with cache_readonly in immutable subclasses + def _isnan(self): + """ return if each value is nan""" + return (self.asi8 == iNaT) + + @property # NB: override with cache_readonly in immutable subclasses + def hasnans(self): + """ return if I have any nans; enables various perf speedups """ + return self._isnan.any() + + def _maybe_mask_results(self, result, fill_value=None, convert=None): + """ + Parameters + ---------- + result : a ndarray + convert : string/dtype or None + + Returns + ------- + result : ndarray with values replace by the fill_value + + mask the result if needed, convert to the provided dtype if its not + None + + This is an internal routine + """ + + if self.hasnans: + if convert: + result = result.astype(convert) + if fill_value is None: + fill_value = np.nan + result[self._isnan] = fill_value + return result + + # ------------------------------------------------------------------ + + @property + def freqstr(self): + """ + Return the frequency object as a string if its set, otherwise None + """ + if self.freq is None: + return None + return self.freq.freqstr + + @property # NB: override with cache_readonly in immutable subclasses + def inferred_freq(self): + """ + Tryies to return a string representing a frequency guess, + generated by infer_freq. Returns None if it can't autodetect the + frequency. + """ + try: + return frequencies.infer_freq(self) + except ValueError: + return None + + # ------------------------------------------------------------------ + # Arithmetic Methods + + def _add_datelike(self, other): + raise TypeError("cannot add {cls} and {typ}" + .format(cls=type(self).__name__, + typ=type(other).__name__)) + + def _sub_datelike(self, other): + raise com.AbstractMethodError(self) + + def _sub_period(self, other): + return NotImplemented + + def _add_delta(self, other): + return NotImplemented + + def _add_delta_td(self, other): + """ + Add a delta of a timedeltalike + return the i8 result view + """ + inc = delta_to_nanoseconds(other) + new_values = checked_add_with_arr(self.asi8, inc, + arr_mask=self._isnan).view('i8') + if self.hasnans: + new_values[self._isnan] = iNaT + return new_values.view('i8') + + def _add_delta_tdi(self, other): + """ + Add a delta of a TimedeltaIndex + return the i8 result view + """ + if not len(self) == len(other): + raise ValueError("cannot add indices of unequal length") + + self_i8 = self.asi8 + other_i8 = other.asi8 + new_values = checked_add_with_arr(self_i8, other_i8, + arr_mask=self._isnan, + b_mask=other._isnan) + if self.hasnans or other.hasnans: + mask = (self._isnan) | (other._isnan) + new_values[mask] = iNaT + return new_values.view('i8') + + +class DatetimeIndexOpsMixin(DatetimeLikeArray): """ common ops mixin to support a unified interface datetimelike Index """ + inferred_freq = cache_readonly(DatetimeLikeArray.inferred_freq.fget) + _isnan = cache_readonly(DatetimeLikeArray._isnan.fget) + hasnans = cache_readonly(DatetimeLikeArray.hasnans.fget) def equals(self, other): """ @@ -152,9 +293,6 @@ def equals(self, other): return np.array_equal(self.asi8, other.asi8) - def __iter__(self): - return (self._box_func(v) for v in self.asi8) - @staticmethod def _join_i8_wrapper(joinf, dtype, with_indexers=True): """ create the join wrapper methods """ @@ -229,13 +367,6 @@ def _ensure_localized(self, result): result = result.tz_localize(self.tz) return result - @property - def _box_func(self): - """ - box function to get object from internal representation - """ - raise com.AbstractMethodError(self) - def _box_values(self, values): """ apply box func to passed values @@ -311,27 +442,6 @@ def __getitem__(self, key): return self._simple_new(result, **attribs) - @property - def freqstr(self): - """ - Return the frequency object as a string if its set, otherwise None - """ - if self.freq is None: - return None - return self.freq.freqstr - - @cache_readonly - def inferred_freq(self): - """ - Tryies to return a string representing a frequency guess, - generated by infer_freq. Returns None if it can't autodetect the - frequency. - """ - try: - return frequencies.infer_freq(self) - except ValueError: - return None - def _nat_new(self, box=True): """ Return Index or ndarray filled with NaT which has the same @@ -424,11 +534,6 @@ def get_duplicates(self): _na_value = NaT """The expected NA value to use with this index.""" - @cache_readonly - def _isnan(self): - """ return if each value is nan""" - return (self.asi8 == iNaT) - @property def asobject(self): """Return object Index which contains boxed values. @@ -449,31 +554,6 @@ def _convert_tolerance(self, tolerance, target): 'target index size') return tolerance - def _maybe_mask_results(self, result, fill_value=None, convert=None): - """ - Parameters - ---------- - result : a ndarray - convert : string/dtype or None - - Returns - ------- - result : ndarray with values replace by the fill_value - - mask the result if needed, convert to the provided dtype if its not - None - - This is an internal routine - """ - - if self.hasnans: - if convert: - result = result.astype(convert) - if fill_value is None: - fill_value = np.nan - result[self._isnan] = fill_value - return result - def tolist(self): """ return a list of the underlying data @@ -630,17 +710,6 @@ def _convert_scalar_indexer(self, key, kind=None): return (super(DatetimeIndexOpsMixin, self) ._convert_scalar_indexer(key, kind=kind)) - def _add_datelike(self, other): - raise TypeError("cannot add {0} and {1}" - .format(type(self).__name__, - type(other).__name__)) - - def _sub_datelike(self, other): - raise com.AbstractMethodError(self) - - def _sub_period(self, other): - return NotImplemented - def _addsub_offset_array(self, other, op): """ Add or subtract array-like of DateOffset objects @@ -802,42 +871,6 @@ def __isub__(self, other): return self.__sub__(other) cls.__isub__ = __isub__ - def _add_delta(self, other): - return NotImplemented - - def _add_delta_td(self, other): - """ - Add a delta of a timedeltalike - return the i8 result view - """ - - inc = delta_to_nanoseconds(other) - new_values = checked_add_with_arr(self.asi8, inc, - arr_mask=self._isnan).view('i8') - if self.hasnans: - new_values[self._isnan] = iNaT - return new_values.view('i8') - - def _add_delta_tdi(self, other): - """ - Add a delta of a TimedeltaIndex - return the i8 result view - """ - - # delta operation - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = checked_add_with_arr(self_i8, other_i8, - arr_mask=self._isnan, - b_mask=other._isnan) - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = iNaT - return new_values.view('i8') - def isin(self, values): """ Compute boolean array of whether each index value is found in the diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 36ea2bffb9531..61fb2ac1ce1b0 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -41,7 +41,7 @@ import pandas.compat as compat from pandas.tseries.frequencies import to_offset, get_period_alias, Resolution from pandas.core.indexes.datetimelike import ( - DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin) + DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) from pandas.tseries.offsets import ( DateOffset, generate_range, Tick, CDay, prefix_mapping) @@ -174,8 +174,92 @@ def _new_DatetimeIndex(cls, d): return result -class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, - Int64Index): +class DatetimeArray(DatetimeLikeArray): + + # ----------------------------------------------------------------- + # Descriptive Properties + + @property + def _box_func(self): + return lambda x: Timestamp(x, freq=self.offset, tz=self.tz) + + @cache_readonly + def dtype(self): + if self.tz is None: + return _NS_DTYPE + return DatetimeTZDtype('ns', self.tz) + + @property + def freq(self): + """get/set the frequency of the Index""" + return self.offset + + @freq.setter + def freq(self, value): + """get/set the frequency of the Index""" + self.offset = value + + @property + def tzinfo(self): + """ + Alias for tz attribute + """ + return self.tz + + @property # NB: override with cache_readonly in immutable subclasses + def _timezone(self): + """ Comparable timezone both for pytz / dateutil""" + return timezones.get_timezone(self.tzinfo) + + # ----------------------------------------------------------------- + # Comparison Methods + + def _has_same_tz(self, other): + zzone = self._timezone + + # vzone sholdn't be None if value is non-datetime like + if isinstance(other, np.datetime64): + # convert to Timestamp as np.datetime64 doesn't have tz attr + other = Timestamp(other) + vzone = timezones.get_timezone(getattr(other, 'tzinfo', '__no_tz__')) + return zzone == vzone + + def _assert_tzawareness_compat(self, other): + # adapted from _Timestamp._assert_tzawareness_compat + other_tz = getattr(other, 'tzinfo', None) + if is_datetime64tz_dtype(other): + # Get tzinfo from Series dtype + other_tz = other.dtype.tz + if other is libts.NaT: + # pd.NaT quacks both aware and naive + pass + elif self.tz is None: + if other_tz is not None: + raise TypeError('Cannot compare tz-naive and tz-aware ' + 'datetime-like objects.') + elif other_tz is None: + raise TypeError('Cannot compare tz-naive and tz-aware ' + 'datetime-like objects') + + # ----------------------------------------------------------------- + # Arithmetic Methods + + def _sub_datelike_dti(self, other): + """subtraction of two DatetimeIndexes""" + if not len(self) == len(other): + raise ValueError("cannot add indices of unequal length") + + self_i8 = self.asi8 + other_i8 = other.asi8 + new_values = self_i8 - other_i8 + if self.hasnans or other.hasnans: + mask = (self._isnan) | (other._isnan) + new_values[mask] = libts.iNaT + return new_values.view('i8') + + +class DatetimeIndex(DatetimeArray, DatelikeOps, TimelikeOps, + DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray of datetime64 data, represented internally as int64, and which can be boxed to Timestamp objects that are subclasses of datetime and @@ -324,6 +408,7 @@ def _add_comparison_methods(cls): _is_numeric_dtype = False _infer_as_myclass = True + _timezone = cache_readonly(DatetimeArray._timezone.fget) @deprecate_kwarg(old_arg_name='infer_dst', new_arg_name='ambiguous', mapping={True: 'infer', False: 'raise'}) @@ -609,10 +694,6 @@ def _generate(cls, start, end, periods, name, offset, index = cls._simple_new(index, name=name, freq=offset, tz=tz) return index - @property - def _box_func(self): - return lambda x: Timestamp(x, freq=self.offset, tz=self.tz) - def _convert_for_op(self, value): """ Convert value to be insertable to ndarray """ if self._has_same_tz(value): @@ -662,23 +743,6 @@ def _simple_new(cls, values, name=None, freq=None, tz=None, result._reset_identity() return result - def _assert_tzawareness_compat(self, other): - # adapted from _Timestamp._assert_tzawareness_compat - other_tz = getattr(other, 'tzinfo', None) - if is_datetime64tz_dtype(other): - # Get tzinfo from Series dtype - other_tz = other.dtype.tz - if other is libts.NaT: - # pd.NaT quacks both aware and naive - pass - elif self.tz is None: - if other_tz is not None: - raise TypeError('Cannot compare tz-naive and tz-aware ' - 'datetime-like objects.') - elif other_tz is None: - raise TypeError('Cannot compare tz-naive and tz-aware ' - 'datetime-like objects') - @property def _values(self): # tz-naive -> ndarray @@ -688,13 +752,6 @@ def _values(self): else: return self.values - @property - def tzinfo(self): - """ - Alias for tz attribute - """ - return self.tz - @property def size(self): # TODO: Remove this when we have a DatetimeTZArray @@ -716,21 +773,6 @@ def nbytes(self): # for TZ-aware return self._ndarray_values.nbytes - @cache_readonly - def _timezone(self): - """ Comparable timezone both for pytz / dateutil""" - return timezones.get_timezone(self.tzinfo) - - def _has_same_tz(self, other): - zzone = self._timezone - - # vzone sholdn't be None if value is non-datetime like - if isinstance(other, np.datetime64): - # convert to Timestamp as np.datetime64 doesn't have tz attr - other = Timestamp(other) - vzone = timezones.get_timezone(getattr(other, 'tzinfo', '__no_tz__')) - return zzone == vzone - @classmethod def _cached_range(cls, start=None, end=None, periods=None, offset=None, name=None): @@ -889,19 +931,6 @@ def _sub_datelike(self, other): .format(typ=type(other).__name__)) return TimedeltaIndex(result) - def _sub_datelike_dti(self, other): - """subtraction of two DatetimeIndexes""" - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = self_i8 - other_i8 - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = libts.iNaT - return new_values.view('i8') - def _maybe_update_attributes(self, attrs): """ Update Index attributes (e.g. freq) depending on op """ freq = attrs.get('freq', None) @@ -1688,16 +1717,6 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): else: raise - @property - def freq(self): - """get/set the frequency of the Index""" - return self.offset - - @freq.setter - def freq(self, value): - """get/set the frequency of the Index""" - self.offset = value - year = _field_accessor('year', 'Y', "The year of the datetime") month = _field_accessor('month', 'M', "The month as January=1, December=12") @@ -1805,12 +1824,6 @@ def inferred_type(self): # sure we can't have ambiguous indexing return 'datetime64' - @cache_readonly - def dtype(self): - if self.tz is None: - return _NS_DTYPE - return DatetimeTZDtype('ns', self.tz) - @property def is_all_dates(self): return True diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index f0567c9c963af..7a513f7e305bb 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -24,7 +24,8 @@ from pandas.tseries.frequencies import get_freq_code as _gfc from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index from pandas.core.indexes.timedeltas import TimedeltaIndex -from pandas.core.indexes.datetimelike import DatelikeOps, DatetimeIndexOpsMixin +from pandas.core.indexes.datetimelike import ( + DatelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) from pandas.core.tools.datetimes import parse_time_string import pandas.tseries.offsets as offsets @@ -123,7 +124,26 @@ def _new_PeriodIndex(cls, **d): return cls._from_ordinals(values=values, **d) -class PeriodIndex(DatelikeOps, DatetimeIndexOpsMixin, Int64Index): +class PeriodArray(DatetimeLikeArray): + @property + def _box_func(self): + return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) + + @cache_readonly + def dtype(self): + return PeriodDtype.construct_from_string(self.freq) + + @property + def _ndarray_values(self): + # Ordinals + return self._data + + @property + def asi8(self): + return self._ndarray_values.view('i8') + + +class PeriodIndex(PeriodArray, DatelikeOps, DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray holding ordinal values indicating regular periods in time such as particular years, quarters, months, etc. @@ -404,10 +424,6 @@ def __contains__(self, key): contains = __contains__ - @property - def asi8(self): - return self._ndarray_values.view('i8') - @cache_readonly def _int64index(self): return Int64Index(self.asi8, name=self.name, fastpath=True) @@ -416,11 +432,6 @@ def _int64index(self): def values(self): return self.astype(object).values - @property - def _ndarray_values(self): - # Ordinals - return self._data - def __array__(self, dtype=None): if is_integer_dtype(dtype): return self.asi8 @@ -461,10 +472,6 @@ def __array_wrap__(self, result, context=None): # cannot pass _simple_new as it is return self._shallow_copy(result, freq=self.freq, name=self.name) - @property - def _box_func(self): - return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) - def _to_embed(self, keep_tz=False, dtype=None): """ return an array repr of this object, potentially casting to object @@ -762,10 +769,6 @@ def shift(self, n): values[self._isnan] = tslib.iNaT return self._shallow_copy(values=values) - @cache_readonly - def dtype(self): - return PeriodDtype.construct_from_string(self.freq) - @property def inferred_type(self): # b/c data is represented as ints make sure we can't have ambiguous diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 6f80962eab079..4146c0ab33b0e 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -28,7 +28,8 @@ import pandas.core.common as com import pandas.core.dtypes.concat as _concat from pandas.util._decorators import Appender, Substitution, deprecate_kwarg -from pandas.core.indexes.datetimelike import TimelikeOps, DatetimeIndexOpsMixin +from pandas.core.indexes.datetimelike import ( + TimelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) from pandas.core.tools.timedeltas import ( to_timedelta, _coerce_scalar_to_timedelta_type) from pandas.tseries.offsets import Tick, DateOffset @@ -97,7 +98,18 @@ def wrapper(self, other): return compat.set_function_name(wrapper, opname, cls) -class TimedeltaIndex(DatetimeIndexOpsMixin, TimelikeOps, Int64Index): +class TimedeltaArray(DatetimeLikeArray): + @property + def _box_func(self): + return lambda x: Timedelta(x, unit='ns') + + @property + def dtype(self): + return _TD_DTYPE + + +class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, + TimelikeOps, Int64Index): """ Immutable ndarray of timedelta64 data, represented internally as int64, and which can be boxed to timedelta objects @@ -315,10 +327,6 @@ def _generate(cls, start, end, periods, name, offset, closed=None): return index - @property - def _box_func(self): - return lambda x: Timedelta(x, unit='ns') - @classmethod def _simple_new(cls, values, name=None, freq=None, **kwargs): values = np.array(values, copy=False) @@ -854,10 +862,6 @@ def is_type_compatible(self, typ): def inferred_type(self): return 'timedelta64' - @property - def dtype(self): - return _TD_DTYPE - @property def is_all_dates(self): return True From 004f13704f6afdb6a5d14ab4619c784501ce5212 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 25 Feb 2018 21:26:55 -0800 Subject: [PATCH 02/13] docstring --- pandas/core/indexes/datetimes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 61fb2ac1ce1b0..8bce8f289a916 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -175,6 +175,12 @@ def _new_DatetimeIndex(cls, d): class DatetimeArray(DatetimeLikeArray): + """ + Assumes that subclass __new__/__init__ defines: + tz + offset + _data + """ # ----------------------------------------------------------------- # Descriptive Properties From 80b525efb1d2991ebb6f151261153424372a0892 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 26 Feb 2018 19:06:21 -0800 Subject: [PATCH 03/13] move classes to array directory --- pandas/core/arrays/datetimelike.py | 149 ++++++++++++++++++++++++++++ pandas/core/arrays/datetimes.py | 103 +++++++++++++++++++ pandas/core/arrays/periods.py | 28 ++++++ pandas/core/arrays/timedeltas.py | 17 ++++ pandas/core/indexes/datetimelike.py | 141 +------------------------- pandas/core/indexes/datetimes.py | 95 +----------------- pandas/core/indexes/period.py | 22 +--- pandas/core/indexes/timedeltas.py | 13 +-- 8 files changed, 305 insertions(+), 263 deletions(-) create mode 100644 pandas/core/arrays/datetimelike.py create mode 100644 pandas/core/arrays/datetimes.py create mode 100644 pandas/core/arrays/periods.py create mode 100644 pandas/core/arrays/timedeltas.py diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py new file mode 100644 index 0000000000000..ffc57cc590382 --- /dev/null +++ b/pandas/core/arrays/datetimelike.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from pandas._libs import iNaT +from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds + +from pandas.tseries import frequencies + +import pandas.core.common as com +from pandas.core.algorithms import checked_add_with_arr + + +class DatetimeLikeArray(object): + """ + Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray + + Assumes that __new__/__init__ defines: + _data + freq + """ + + @property + def _box_func(self): + """ + box function to get object from internal representation + """ + raise com.AbstractMethodError(self) + + def __iter__(self): + return (self._box_func(v) for v in self.asi8) + + @property + def values(self): + """ return the underlying data as an ndarray """ + return self._data.view(np.ndarray) + + @property + def asi8(self): + # do not cache or you'll create a memory leak + return self.values.view('i8') + + # ------------------------------------------------------------------ + # Null Handling + + @property # NB: override with cache_readonly in immutable subclasses + def _isnan(self): + """ return if each value is nan""" + return (self.asi8 == iNaT) + + @property # NB: override with cache_readonly in immutable subclasses + def hasnans(self): + """ return if I have any nans; enables various perf speedups """ + return self._isnan.any() + + def _maybe_mask_results(self, result, fill_value=None, convert=None): + """ + Parameters + ---------- + result : a ndarray + convert : string/dtype or None + + Returns + ------- + result : ndarray with values replace by the fill_value + + mask the result if needed, convert to the provided dtype if its not + None + + This is an internal routine + """ + + if self.hasnans: + if convert: + result = result.astype(convert) + if fill_value is None: + fill_value = np.nan + result[self._isnan] = fill_value + return result + + # ------------------------------------------------------------------ + + @property + def freqstr(self): + """ + Return the frequency object as a string if its set, otherwise None + """ + if self.freq is None: + return None + return self.freq.freqstr + + @property # NB: override with cache_readonly in immutable subclasses + def inferred_freq(self): + """ + Tryies to return a string representing a frequency guess, + generated by infer_freq. Returns None if it can't autodetect the + frequency. + """ + try: + return frequencies.infer_freq(self) + except ValueError: + return None + + # ------------------------------------------------------------------ + # Arithmetic Methods + + def _add_datelike(self, other): + raise TypeError("cannot add {cls} and {typ}" + .format(cls=type(self).__name__, + typ=type(other).__name__)) + + def _sub_datelike(self, other): + raise com.AbstractMethodError(self) + + def _sub_period(self, other): + return NotImplemented + + def _add_delta(self, other): + return NotImplemented + + def _add_delta_td(self, other): + """ + Add a delta of a timedeltalike + return the i8 result view + """ + inc = delta_to_nanoseconds(other) + new_values = checked_add_with_arr(self.asi8, inc, + arr_mask=self._isnan).view('i8') + if self.hasnans: + new_values[self._isnan] = iNaT + return new_values.view('i8') + + def _add_delta_tdi(self, other): + """ + Add a delta of a TimedeltaIndex + return the i8 result view + """ + if not len(self) == len(other): + raise ValueError("cannot add indices of unequal length") + + self_i8 = self.asi8 + other_i8 = other.asi8 + new_values = checked_add_with_arr(self_i8, other_i8, + arr_mask=self._isnan, + b_mask=other._isnan) + if self.hasnans or other.hasnans: + mask = (self._isnan) | (other._isnan) + new_values[mask] = iNaT + return new_values.view('i8') diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py new file mode 100644 index 0000000000000..dd6f1b6549090 --- /dev/null +++ b/pandas/core/arrays/datetimes.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from pandas._libs.tslib import Timestamp, NaT, iNaT +from pandas._libs.tslibs import timezones + +from pandas.util._decorators import cache_readonly + +from pandas.core.dtypes.common import _NS_DTYPE, is_datetime64tz_dtype +from pandas.core.dtypes.dtypes import DatetimeTZDtype + +from .datetimelike import DatetimeLikeArray + + +class DatetimeArray(DatetimeLikeArray): + """ + Assumes that subclass __new__/__init__ defines: + tz + offset + _data + """ + + # ----------------------------------------------------------------- + # Descriptive Properties + + @property + def _box_func(self): + return lambda x: Timestamp(x, freq=self.offset, tz=self.tz) + + @cache_readonly + def dtype(self): + if self.tz is None: + return _NS_DTYPE + return DatetimeTZDtype('ns', self.tz) + + @property + def freq(self): + """get/set the frequency of the Index""" + return self.offset + + @freq.setter + def freq(self, value): + """get/set the frequency of the Index""" + self.offset = value + + @property + def tzinfo(self): + """ + Alias for tz attribute + """ + return self.tz + + @property # NB: override with cache_readonly in immutable subclasses + def _timezone(self): + """ Comparable timezone both for pytz / dateutil""" + return timezones.get_timezone(self.tzinfo) + + # ----------------------------------------------------------------- + # Comparison Methods + + def _has_same_tz(self, other): + zzone = self._timezone + + # vzone sholdn't be None if value is non-datetime like + if isinstance(other, np.datetime64): + # convert to Timestamp as np.datetime64 doesn't have tz attr + other = Timestamp(other) + vzone = timezones.get_timezone(getattr(other, 'tzinfo', '__no_tz__')) + return zzone == vzone + + def _assert_tzawareness_compat(self, other): + # adapted from _Timestamp._assert_tzawareness_compat + other_tz = getattr(other, 'tzinfo', None) + if is_datetime64tz_dtype(other): + # Get tzinfo from Series dtype + other_tz = other.dtype.tz + if other is NaT: + # pd.NaT quacks both aware and naive + pass + elif self.tz is None: + if other_tz is not None: + raise TypeError('Cannot compare tz-naive and tz-aware ' + 'datetime-like objects.') + elif other_tz is None: + raise TypeError('Cannot compare tz-naive and tz-aware ' + 'datetime-like objects') + + # ----------------------------------------------------------------- + # Arithmetic Methods + + def _sub_datelike_dti(self, other): + """subtraction of two DatetimeIndexes""" + if not len(self) == len(other): + raise ValueError("cannot add indices of unequal length") + + self_i8 = self.asi8 + other_i8 = other.asi8 + new_values = self_i8 - other_i8 + if self.hasnans or other.hasnans: + mask = (self._isnan) | (other._isnan) + new_values[mask] = iNaT + return new_values.view('i8') diff --git a/pandas/core/arrays/periods.py b/pandas/core/arrays/periods.py new file mode 100644 index 0000000000000..ed0c356dabbed --- /dev/null +++ b/pandas/core/arrays/periods.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +from pandas._libs.tslibs.period import Period + +from pandas.util._decorators import cache_readonly + +from pandas.core.dtypes.dtypes import PeriodDtype + +from .datetimelike import DatetimeLikeArray + + +class PeriodArray(DatetimeLikeArray): + @property + def _box_func(self): + return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) + + @cache_readonly + def dtype(self): + return PeriodDtype.construct_from_string(self.freq) + + @property + def _ndarray_values(self): + # Ordinals + return self._data + + @property + def asi8(self): + return self._ndarray_values.view('i8') diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py new file mode 100644 index 0000000000000..9135499bfa995 --- /dev/null +++ b/pandas/core/arrays/timedeltas.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from pandas._libs.tslib import Timedelta + +from pandas.core.dtypes.common import _TD_DTYPE + +from .datetimelike import DatetimeLikeArray + + +class TimedeltaArray(DatetimeLikeArray): + @property + def _box_func(self): + return lambda x: Timedelta(x, unit='ns') + + @property + def dtype(self): + return _TD_DTYPE diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 387399771ea8c..f699c66eaa39a 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -13,7 +13,6 @@ from pandas._libs import lib, iNaT, NaT from pandas._libs.tslibs.period import Period -from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds from pandas._libs.tslibs.timestamps import round_ns from pandas.core.dtypes.common import ( @@ -39,10 +38,10 @@ ABCIndex, ABCSeries, ABCPeriodIndex, ABCIndexClass) from pandas.core.dtypes.missing import isna from pandas.core import common as com, algorithms, ops -from pandas.core.algorithms import checked_add_with_arr from pandas.errors import NullFrequencyError, PerformanceWarning import pandas.io.formats.printing as printing +from pandas.core.arrays.datetimelike import DatetimeLikeArray from pandas.core.indexes.base import Index, _index_shared_docs from pandas.util._decorators import Appender, cache_readonly import pandas.core.dtypes.concat as _concat @@ -122,144 +121,6 @@ def ceil(self, freq): return self._round(freq, np.ceil) -class DatetimeLikeArray(object): - """ - Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray - - Assumes that __new__/__init__ defines: - _data - freq - """ - - @property - def _box_func(self): - """ - box function to get object from internal representation - """ - raise com.AbstractMethodError(self) - - def __iter__(self): - return (self._box_func(v) for v in self.asi8) - - @property - def values(self): - """ return the underlying data as an ndarray """ - return self._data.view(np.ndarray) - - @property - def asi8(self): - # do not cache or you'll create a memory leak - return self.values.view('i8') - - # ------------------------------------------------------------------ - # Null Handling - - @property # NB: override with cache_readonly in immutable subclasses - def _isnan(self): - """ return if each value is nan""" - return (self.asi8 == iNaT) - - @property # NB: override with cache_readonly in immutable subclasses - def hasnans(self): - """ return if I have any nans; enables various perf speedups """ - return self._isnan.any() - - def _maybe_mask_results(self, result, fill_value=None, convert=None): - """ - Parameters - ---------- - result : a ndarray - convert : string/dtype or None - - Returns - ------- - result : ndarray with values replace by the fill_value - - mask the result if needed, convert to the provided dtype if its not - None - - This is an internal routine - """ - - if self.hasnans: - if convert: - result = result.astype(convert) - if fill_value is None: - fill_value = np.nan - result[self._isnan] = fill_value - return result - - # ------------------------------------------------------------------ - - @property - def freqstr(self): - """ - Return the frequency object as a string if its set, otherwise None - """ - if self.freq is None: - return None - return self.freq.freqstr - - @property # NB: override with cache_readonly in immutable subclasses - def inferred_freq(self): - """ - Tryies to return a string representing a frequency guess, - generated by infer_freq. Returns None if it can't autodetect the - frequency. - """ - try: - return frequencies.infer_freq(self) - except ValueError: - return None - - # ------------------------------------------------------------------ - # Arithmetic Methods - - def _add_datelike(self, other): - raise TypeError("cannot add {cls} and {typ}" - .format(cls=type(self).__name__, - typ=type(other).__name__)) - - def _sub_datelike(self, other): - raise com.AbstractMethodError(self) - - def _sub_period(self, other): - return NotImplemented - - def _add_delta(self, other): - return NotImplemented - - def _add_delta_td(self, other): - """ - Add a delta of a timedeltalike - return the i8 result view - """ - inc = delta_to_nanoseconds(other) - new_values = checked_add_with_arr(self.asi8, inc, - arr_mask=self._isnan).view('i8') - if self.hasnans: - new_values[self._isnan] = iNaT - return new_values.view('i8') - - def _add_delta_tdi(self, other): - """ - Add a delta of a TimedeltaIndex - return the i8 result view - """ - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = checked_add_with_arr(self_i8, other_i8, - arr_mask=self._isnan, - b_mask=other._isnan) - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = iNaT - return new_values.view('i8') - - class DatetimeIndexOpsMixin(DatetimeLikeArray): """ common ops mixin to support a unified interface datetimelike Index """ inferred_freq = cache_readonly(DatetimeLikeArray.inferred_freq.fget) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 7c55599be6cfa..91d771aa820fd 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -13,7 +13,7 @@ _INT64_DTYPE, _NS_DTYPE, is_object_dtype, - is_datetime64_dtype, is_datetime64tz_dtype, + is_datetime64_dtype, is_datetimetz, is_dtype_equal, is_timedelta64_dtype, @@ -35,13 +35,14 @@ import pandas.core.dtypes.concat as _concat from pandas.errors import PerformanceWarning from pandas.core.algorithms import checked_add_with_arr +from pandas.core.arrays.datetimes import DatetimeArray from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.numeric import Int64Index, Float64Index import pandas.compat as compat from pandas.tseries.frequencies import to_offset, get_period_alias, Resolution from pandas.core.indexes.datetimelike import ( - DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) + DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin) from pandas.tseries.offsets import ( DateOffset, generate_range, Tick, CDay, prefix_mapping) @@ -174,96 +175,6 @@ def _new_DatetimeIndex(cls, d): return result -class DatetimeArray(DatetimeLikeArray): - """ - Assumes that subclass __new__/__init__ defines: - tz - offset - _data - """ - - # ----------------------------------------------------------------- - # Descriptive Properties - - @property - def _box_func(self): - return lambda x: Timestamp(x, freq=self.offset, tz=self.tz) - - @cache_readonly - def dtype(self): - if self.tz is None: - return _NS_DTYPE - return DatetimeTZDtype('ns', self.tz) - - @property - def freq(self): - """get/set the frequency of the Index""" - return self.offset - - @freq.setter - def freq(self, value): - """get/set the frequency of the Index""" - self.offset = value - - @property - def tzinfo(self): - """ - Alias for tz attribute - """ - return self.tz - - @property # NB: override with cache_readonly in immutable subclasses - def _timezone(self): - """ Comparable timezone both for pytz / dateutil""" - return timezones.get_timezone(self.tzinfo) - - # ----------------------------------------------------------------- - # Comparison Methods - - def _has_same_tz(self, other): - zzone = self._timezone - - # vzone sholdn't be None if value is non-datetime like - if isinstance(other, np.datetime64): - # convert to Timestamp as np.datetime64 doesn't have tz attr - other = Timestamp(other) - vzone = timezones.get_timezone(getattr(other, 'tzinfo', '__no_tz__')) - return zzone == vzone - - def _assert_tzawareness_compat(self, other): - # adapted from _Timestamp._assert_tzawareness_compat - other_tz = getattr(other, 'tzinfo', None) - if is_datetime64tz_dtype(other): - # Get tzinfo from Series dtype - other_tz = other.dtype.tz - if other is libts.NaT: - # pd.NaT quacks both aware and naive - pass - elif self.tz is None: - if other_tz is not None: - raise TypeError('Cannot compare tz-naive and tz-aware ' - 'datetime-like objects.') - elif other_tz is None: - raise TypeError('Cannot compare tz-naive and tz-aware ' - 'datetime-like objects') - - # ----------------------------------------------------------------- - # Arithmetic Methods - - def _sub_datelike_dti(self, other): - """subtraction of two DatetimeIndexes""" - if not len(self) == len(other): - raise ValueError("cannot add indices of unequal length") - - self_i8 = self.asi8 - other_i8 = other.asi8 - new_values = self_i8 - other_i8 - if self.hasnans or other.hasnans: - mask = (self._isnan) | (other._isnan) - new_values[mask] = libts.iNaT - return new_values.view('i8') - - class DatetimeIndex(DatetimeArray, DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, Int64Index): """ diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 7a513f7e305bb..70f58c1b51eb8 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -25,7 +25,7 @@ from pandas.core.indexes.datetimes import DatetimeIndex, Int64Index, Index from pandas.core.indexes.timedeltas import TimedeltaIndex from pandas.core.indexes.datetimelike import ( - DatelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) + DatelikeOps, DatetimeIndexOpsMixin) from pandas.core.tools.datetimes import parse_time_string import pandas.tseries.offsets as offsets @@ -38,6 +38,7 @@ from pandas._libs.tslibs import resolution, period from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds +from pandas.core.arrays.periods import PeriodArray from pandas.core.base import _shared_docs from pandas.core.indexes.base import _index_shared_docs, _ensure_index @@ -124,25 +125,6 @@ def _new_PeriodIndex(cls, **d): return cls._from_ordinals(values=values, **d) -class PeriodArray(DatetimeLikeArray): - @property - def _box_func(self): - return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) - - @cache_readonly - def dtype(self): - return PeriodDtype.construct_from_string(self.freq) - - @property - def _ndarray_values(self): - # Ordinals - return self._data - - @property - def asi8(self): - return self._ndarray_values.view('i8') - - class PeriodIndex(PeriodArray, DatelikeOps, DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray holding ordinal values indicating regular periods in diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 9a1b476627fa5..46515cad6d344 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -17,6 +17,7 @@ from pandas.core.dtypes.missing import isna from pandas.core.dtypes.generic import ABCSeries +from pandas.core.arrays.timedeltas import TimedeltaArray from pandas.core.indexes.base import Index from pandas.core.indexes.numeric import Int64Index import pandas.compat as compat @@ -29,7 +30,7 @@ import pandas.core.dtypes.concat as _concat from pandas.util._decorators import Appender, Substitution, deprecate_kwarg from pandas.core.indexes.datetimelike import ( - TimelikeOps, DatetimeIndexOpsMixin, DatetimeLikeArray) + TimelikeOps, DatetimeIndexOpsMixin) from pandas.core.tools.timedeltas import ( to_timedelta, _coerce_scalar_to_timedelta_type) from pandas.tseries.offsets import Tick, DateOffset @@ -96,16 +97,6 @@ def wrapper(self, other): return compat.set_function_name(wrapper, opname, cls) -class TimedeltaArray(DatetimeLikeArray): - @property - def _box_func(self): - return lambda x: Timedelta(x, unit='ns') - - @property - def dtype(self): - return _TD_DTYPE - - class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, TimelikeOps, Int64Index): """ From b91ddacad013fde9435bbb1b536cd598746cc46d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 28 Feb 2018 17:16:24 -0800 Subject: [PATCH 04/13] flake8 fixup remove unused import --- pandas/core/indexes/period.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index b5bcfdf0af3c2..44659cc0ce52f 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -16,7 +16,6 @@ is_bool_dtype, pandas_dtype, _ensure_object) -from pandas.core.dtypes.dtypes import PeriodDtype from pandas.core.dtypes.generic import ABCSeries import pandas.tseries.frequencies as frequencies From 3a67bce8169663430005b2a36673132fc1e79f4c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 18 Mar 2018 10:12:52 -0700 Subject: [PATCH 05/13] comments for cache_readonlys, append Mixin to name, add imports to __init__ --- pandas/core/arrays/__init__.py | 3 +++ pandas/core/arrays/datetimelike.py | 2 +- pandas/core/arrays/datetimes.py | 4 ++-- pandas/core/arrays/periods.py | 4 ++-- pandas/core/arrays/timedeltas.py | 4 ++-- pandas/core/indexes/datetimelike.py | 14 +++++++++----- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index f8adcf520c15b..e7d341004eed7 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -1,2 +1,5 @@ from .base import ExtensionArray # noqa from .categorical import Categorical # noqa +from .datetimes import DatetimeArray # noqa +from .periods import PeriodArray # noqa +from .timedeltas import TimedeltaArray # noqa diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 1a34dcac9b515..6e265b1b66558 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -11,7 +11,7 @@ from pandas.core.algorithms import checked_add_with_arr -class DatetimeLikeArray(object): +class DatetimeLikeArrayMixin(object): """ Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index c2384425fc892..6e49ca9bfd5a5 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -10,10 +10,10 @@ from pandas.core.dtypes.common import _NS_DTYPE, is_datetime64tz_dtype from pandas.core.dtypes.dtypes import DatetimeTZDtype -from .datetimelike import DatetimeLikeArray +from .datetimelike import DatetimeLikeArrayMixin -class DatetimeArray(DatetimeLikeArray): +class DatetimeArray(DatetimeLikeArrayMixin): """ Assumes that subclass __new__/__init__ defines: tz diff --git a/pandas/core/arrays/periods.py b/pandas/core/arrays/periods.py index ed0c356dabbed..afdb0924fdc37 100644 --- a/pandas/core/arrays/periods.py +++ b/pandas/core/arrays/periods.py @@ -6,10 +6,10 @@ from pandas.core.dtypes.dtypes import PeriodDtype -from .datetimelike import DatetimeLikeArray +from .datetimelike import DatetimeLikeArrayMixin -class PeriodArray(DatetimeLikeArray): +class PeriodArray(DatetimeLikeArrayMixin): @property def _box_func(self): return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 9135499bfa995..45cf95da4ef09 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -4,10 +4,10 @@ from pandas.core.dtypes.common import _TD_DTYPE -from .datetimelike import DatetimeLikeArray +from .datetimelike import DatetimeLikeArrayMixin -class TimedeltaArray(DatetimeLikeArray): +class TimedeltaArray(DatetimeLikeArrayMixin): @property def _box_func(self): return lambda x: Timedelta(x, unit='ns') diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 2b3847a8ecb73..f0b1193ad4f52 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -42,7 +42,7 @@ from pandas.errors import NullFrequencyError, PerformanceWarning import pandas.io.formats.printing as printing -from pandas.core.arrays.datetimelike import DatetimeLikeArray +from pandas.core.arrays.datetimelike import DatetimeLikeArrayMixin from pandas.core.indexes.base import Index, _index_shared_docs from pandas.util._decorators import Appender, cache_readonly import pandas.core.dtypes.concat as _concat @@ -209,11 +209,15 @@ def ceil(self, freq): return self._round(freq, np.ceil) -class DatetimeIndexOpsMixin(DatetimeLikeArray): +class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin): """ common ops mixin to support a unified interface datetimelike Index """ - inferred_freq = cache_readonly(DatetimeLikeArray.inferred_freq.fget) - _isnan = cache_readonly(DatetimeLikeArray._isnan.fget) - hasnans = cache_readonly(DatetimeLikeArray.hasnans.fget) + + # DatetimeLikeArrayMixin assumes subclasses are mutable, so these are + # properties there. They can be made into cache_readonly for Index + # subclasses bc they are immutable + inferred_freq = cache_readonly(DatetimeLikeArrayMixin.inferred_freq.fget) + _isnan = cache_readonly(DatetimeLikeArrayMixin._isnan.fget) + hasnans = cache_readonly(DatetimeLikeArrayMixin.hasnans.fget) def equals(self, other): """ From 6b17031becd9346131a8202defbe12935d3cdb9e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 16 May 2018 21:29:35 -0700 Subject: [PATCH 06/13] fix merge screwup --- pandas/core/arrays/datetimelike.py | 19 ++++++++++++++++++- pandas/core/arrays/datetimes.py | 10 ---------- pandas/core/indexes/datetimelike.py | 13 ------------- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 6e265b1b66558..180417ce5e49c 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -17,7 +17,10 @@ class DatetimeLikeArrayMixin(object): Assumes that __new__/__init__ defines: _data - freq + _freq + + and that the inheriting class has methods: + _validate_frequency """ @property @@ -79,6 +82,20 @@ def _maybe_mask_results(self, result, fill_value=None, convert=None): return result # ------------------------------------------------------------------ + # Frequency Properties/Methods + + @property + def freq(self): + """Return the frequency object if it is set, otherwise None""" + return self._freq + + @freq.setter + def freq(self, value): + if value is not None: + value = frequencies.to_offset(value) + self._validate_frequency(self, value) + + self._freq = value @property def freqstr(self): diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index 74f1cef69c735..aadfb7fa0016b 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -47,16 +47,6 @@ def _timezone(self): """ Comparable timezone both for pytz / dateutil""" return timezones.get_timezone(self.tzinfo) - @property - def freq(self): - """get/set the frequency of the instance""" - return self._freq - - @freq.setter - def freq(self, value): - """get/set the frequency of the instance""" - self._freq = value - @property def offset(self): """get/set the frequency of the instance""" diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index 0ee5fe042213d..157dcb223230b 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -228,19 +228,6 @@ def _validate_frequency(cls, index, freq, **kwargs): 'conform to passed frequency {passed}') raise ValueError(msg.format(infer=inferred, passed=freq.freqstr)) - @property - def freq(self): - """Return the frequency object if it is set, otherwise None""" - return self._freq - - @freq.setter - def freq(self, value): - if value is not None: - value = frequencies.to_offset(value) - self._validate_frequency(self, value) - - self._freq = value - class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin): """ common ops mixin to support a unified interface datetimelike Index """ From a055d408e5b55bd8afdeae236ad3f8ab9d12024b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 25 May 2018 10:46:06 -0700 Subject: [PATCH 07/13] rename arrays to FooArray Mixin --- pandas/core/arrays/datetimes.py | 2 +- pandas/core/arrays/periods.py | 2 +- pandas/core/arrays/timedeltas.py | 2 +- pandas/core/indexes/datetimes.py | 4 ++-- pandas/core/indexes/period.py | 5 +++-- pandas/core/indexes/timedeltas.py | 4 ++-- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/pandas/core/arrays/datetimes.py b/pandas/core/arrays/datetimes.py index aadfb7fa0016b..fb51f3324c5ea 100644 --- a/pandas/core/arrays/datetimes.py +++ b/pandas/core/arrays/datetimes.py @@ -14,7 +14,7 @@ from .datetimelike import DatetimeLikeArrayMixin -class DatetimeArray(DatetimeLikeArrayMixin): +class DatetimeArrayMixin(DatetimeLikeArrayMixin): """ Assumes that subclass __new__/__init__ defines: tz diff --git a/pandas/core/arrays/periods.py b/pandas/core/arrays/periods.py index afdb0924fdc37..1158bae748f0a 100644 --- a/pandas/core/arrays/periods.py +++ b/pandas/core/arrays/periods.py @@ -9,7 +9,7 @@ from .datetimelike import DatetimeLikeArrayMixin -class PeriodArray(DatetimeLikeArrayMixin): +class PeriodArrayMixin(DatetimeLikeArrayMixin): @property def _box_func(self): return lambda x: Period._from_ordinal(ordinal=x, freq=self.freq) diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedeltas.py index 45cf95da4ef09..487858c49b66a 100644 --- a/pandas/core/arrays/timedeltas.py +++ b/pandas/core/arrays/timedeltas.py @@ -7,7 +7,7 @@ from .datetimelike import DatetimeLikeArrayMixin -class TimedeltaArray(DatetimeLikeArrayMixin): +class TimedeltaArrayMixin(DatetimeLikeArrayMixin): @property def _box_func(self): return lambda x: Timedelta(x, unit='ns') diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 39dc9df90f82b..587bdbdbe33d1 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -35,7 +35,7 @@ import pandas.core.dtypes.concat as _concat from pandas.errors import PerformanceWarning from pandas.core.algorithms import checked_add_with_arr -from pandas.core.arrays.datetimes import DatetimeArray +from pandas.core.arrays.datetimes import DatetimeArrayMixin from pandas.core.indexes.base import Index, _index_shared_docs from pandas.core.indexes.numeric import Int64Index, Float64Index @@ -173,7 +173,7 @@ def _new_DatetimeIndex(cls, d): return result -class DatetimeIndex(DatetimeArray, DatelikeOps, TimelikeOps, +class DatetimeIndex(DatetimeArrayMixin, DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, Int64Index): """ Immutable ndarray of datetime64 data, represented internally as int64, and diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 32a28a92c16ef..4674a51c974ff 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -35,7 +35,7 @@ from pandas._libs.tslibs import resolution, period from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds -from pandas.core.arrays.periods import PeriodArray +from pandas.core.arrays.periods import PeriodArrayMixin from pandas.core.base import _shared_docs from pandas.core.indexes.base import _index_shared_docs, _ensure_index @@ -122,7 +122,8 @@ def _new_PeriodIndex(cls, **d): return cls._from_ordinals(values=values, **d) -class PeriodIndex(PeriodArray, DatelikeOps, DatetimeIndexOpsMixin, Int64Index): +class PeriodIndex(PeriodArrayMixin, DatelikeOps, DatetimeIndexOpsMixin, + Int64Index): """ Immutable ndarray holding ordinal values indicating regular periods in time such as particular years, quarters, months, etc. diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index d4caa6c11602b..bf13a781e5f5a 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -17,7 +17,7 @@ from pandas.core.dtypes.missing import isna from pandas.core.dtypes.generic import ABCSeries -from pandas.core.arrays.timedeltas import TimedeltaArray +from pandas.core.arrays.timedeltas import TimedeltaArrayMixin from pandas.core.indexes.base import Index from pandas.core.indexes.numeric import Int64Index import pandas.compat as compat @@ -97,7 +97,7 @@ def wrapper(self, other): return compat.set_function_name(wrapper, opname, cls) -class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, +class TimedeltaIndex(TimedeltaArrayMixin, DatetimeIndexOpsMixin, TimelikeOps, Int64Index): """ Immutable ndarray of timedelta64 data, represented internally as int64, and From d1faeb69359bd50b7bc6d5288904d06c870e4ea1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 25 May 2018 11:10:41 -0700 Subject: [PATCH 08/13] fixups imports --- pandas/core/arrays/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index e7d341004eed7..f2e2d51d51a5c 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -1,5 +1,5 @@ from .base import ExtensionArray # noqa from .categorical import Categorical # noqa -from .datetimes import DatetimeArray # noqa -from .periods import PeriodArray # noqa -from .timedeltas import TimedeltaArray # noqa +from .datetimes import DatetimeArrayMixin # noqa +from .periods import PeriodArrayMixin # noqa +from .timedeltas import TimedeltaArrayMixin # noqa From fcb8d6a64388c69c717e500466fd1e2c33ef70d6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 25 May 2018 12:02:44 -0700 Subject: [PATCH 09/13] fixup namerror --- pandas/core/indexes/datetimes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 587bdbdbe33d1..676c146f432db 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -326,7 +326,7 @@ def _add_comparison_methods(cls): _is_numeric_dtype = False _infer_as_myclass = True - _timezone = cache_readonly(DatetimeArray._timezone.fget) + _timezone = cache_readonly(DatetimeArrayMixin._timezone.fget) def __new__(cls, data=None, freq=None, start=None, end=None, periods=None, tz=None, From 71dfe08bf4b8e35284277f8cd500db41951f9b38 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 4 Jun 2018 07:17:26 -0700 Subject: [PATCH 10/13] fixup missing import --- pandas/core/indexes/datetimelike.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index ee85567a481c6..57085b07f26f6 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -40,6 +40,7 @@ ABCIndex, ABCSeries, ABCDataFrame, ABCPeriodIndex, ABCIndexClass) from pandas.core.dtypes.missing import isna from pandas.core import common as com, algorithms, ops +from pandas.core.algorithms import checked_add_with_arr from pandas.errors import NullFrequencyError, PerformanceWarning import pandas.io.formats.printing as printing From 0d4f48a0f266e43e539e6f03cf9b2bd4978ae5a1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 22 Jun 2018 11:06:13 -0700 Subject: [PATCH 11/13] reorder imports --- pandas/core/arrays/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index f2e2d51d51a5c..06fa6811647ab 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -1,5 +1,5 @@ from .base import ExtensionArray # noqa from .categorical import Categorical # noqa from .datetimes import DatetimeArrayMixin # noqa -from .periods import PeriodArrayMixin # noqa from .timedeltas import TimedeltaArrayMixin # noqa +from .periods import PeriodArrayMixin # noqa From 1b910c79630e6b552e6c2fd52b79d727cb06947f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 22 Jun 2018 13:06:44 -0700 Subject: [PATCH 12/13] De-pluralize --- pandas/core/arrays/__init__.py | 4 ++-- pandas/core/arrays/{periods.py => period.py} | 0 pandas/core/arrays/{timedeltas.py => timedelta.py} | 0 pandas/core/indexes/period.py | 2 +- pandas/core/indexes/timedeltas.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename pandas/core/arrays/{periods.py => period.py} (100%) rename pandas/core/arrays/{timedeltas.py => timedelta.py} (100%) diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index 06fa6811647ab..61606cc7c360a 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -1,5 +1,5 @@ from .base import ExtensionArray # noqa from .categorical import Categorical # noqa from .datetimes import DatetimeArrayMixin # noqa -from .timedeltas import TimedeltaArrayMixin # noqa -from .periods import PeriodArrayMixin # noqa +from .period import PeriodArrayMixin # noqa +from .timedelta import TimedeltaArrayMixin # noqa diff --git a/pandas/core/arrays/periods.py b/pandas/core/arrays/period.py similarity index 100% rename from pandas/core/arrays/periods.py rename to pandas/core/arrays/period.py diff --git a/pandas/core/arrays/timedeltas.py b/pandas/core/arrays/timedelta.py similarity index 100% rename from pandas/core/arrays/timedeltas.py rename to pandas/core/arrays/timedelta.py diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 0e93c1b8533ae..5c32acccdd261 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -35,7 +35,7 @@ from pandas._libs.tslibs import resolution, period from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds -from pandas.core.arrays.periods import PeriodArrayMixin +from pandas.core.arrays.period import PeriodArrayMixin from pandas.core.base import _shared_docs from pandas.core.indexes.base import _index_shared_docs, _ensure_index diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 2788ad224fa6a..8b62d9aa631e9 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -17,7 +17,7 @@ from pandas.core.dtypes.missing import isna from pandas.core.dtypes.generic import ABCSeries -from pandas.core.arrays.timedeltas import TimedeltaArrayMixin +from pandas.core.arrays.timedelta import TimedeltaArrayMixin from pandas.core.indexes.base import Index from pandas.core.indexes.numeric import Int64Index import pandas.compat as compat From ed8304619f8d1c465a718b568140ba0bef28d163 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 29 Jun 2018 10:28:53 -0700 Subject: [PATCH 13/13] fixup remove unused import --- pandas/core/indexes/datetimelike.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pandas/core/indexes/datetimelike.py b/pandas/core/indexes/datetimelike.py index af113c9595358..328c51aae1807 100644 --- a/pandas/core/indexes/datetimelike.py +++ b/pandas/core/indexes/datetimelike.py @@ -15,7 +15,6 @@ from pandas._libs import lib, iNaT, NaT, Timedelta from pandas._libs.tslibs.period import (Period, IncompatibleFrequency, _DIFFERENT_FREQ_INDEX) -from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds from pandas._libs.tslibs.timestamps import round_ns from pandas.core.dtypes.common import (