Skip to content

Commit eb95979

Browse files
jbrockmendeljreback
authored andcommitted
move PeriodIndex comparisons, implement PeriodArray constructor (#21798)
1 parent f37a6a2 commit eb95979

File tree

8 files changed

+238
-193
lines changed

8 files changed

+238
-193
lines changed

pandas/core/arrays/datetimelike.py

+57-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
# -*- coding: utf-8 -*-
2+
import operator
23

34
import numpy as np
45

56
from pandas._libs import lib, iNaT, NaT
6-
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
7+
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds, Timedelta
78
from pandas._libs.tslibs.period import (
89
DIFFERENT_FREQ_INDEX, IncompatibleFrequency)
910

11+
from pandas.errors import NullFrequencyError
12+
1013
from pandas.tseries import frequencies
14+
from pandas.tseries.offsets import Tick
1115

12-
from pandas.core.dtypes.common import is_period_dtype
16+
from pandas.core.dtypes.common import is_period_dtype, is_timedelta64_dtype
1317
import pandas.core.common as com
1418
from pandas.core.algorithms import checked_add_with_arr
1519

@@ -130,6 +134,17 @@ def inferred_freq(self):
130134
except ValueError:
131135
return None
132136

137+
@property # NB: override with cache_readonly in immutable subclasses
138+
def _resolution(self):
139+
return frequencies.Resolution.get_reso_from_freq(self.freqstr)
140+
141+
@property # NB: override with cache_readonly in immutable subclasses
142+
def resolution(self):
143+
"""
144+
Returns day, hour, minute, second, millisecond or microsecond
145+
"""
146+
return frequencies.Resolution.get_str(self._resolution)
147+
133148
# ------------------------------------------------------------------
134149
# Arithmetic Methods
135150

@@ -228,3 +243,43 @@ def _sub_period_array(self, other):
228243
mask = (self._isnan) | (other._isnan)
229244
new_values[mask] = NaT
230245
return new_values
246+
247+
def _addsub_int_array(self, other, op):
248+
"""
249+
Add or subtract array-like of integers equivalent to applying
250+
`shift` pointwise.
251+
252+
Parameters
253+
----------
254+
other : Index, ExtensionArray, np.ndarray
255+
integer-dtype
256+
op : {operator.add, operator.sub}
257+
258+
Returns
259+
-------
260+
result : same class as self
261+
"""
262+
assert op in [operator.add, operator.sub]
263+
if is_period_dtype(self):
264+
# easy case for PeriodIndex
265+
if op is operator.sub:
266+
other = -other
267+
res_values = checked_add_with_arr(self.asi8, other,
268+
arr_mask=self._isnan)
269+
res_values = res_values.view('i8')
270+
res_values[self._isnan] = iNaT
271+
return self._from_ordinals(res_values, freq=self.freq)
272+
273+
elif self.freq is None:
274+
# GH#19123
275+
raise NullFrequencyError("Cannot shift with no freq")
276+
277+
elif isinstance(self.freq, Tick):
278+
# easy case where we can convert to timedelta64 operation
279+
td = Timedelta(self.freq)
280+
return op(self, td * other)
281+
282+
# We should only get here with DatetimeIndex; dispatch
283+
# to _addsub_offset_array
284+
assert not is_timedelta64_dtype(self)
285+
return op(self, np.array(other) * self.freq)

pandas/core/arrays/datetimes.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -94,19 +94,28 @@ def _timezone(self):
9494
@property
9595
def offset(self):
9696
"""get/set the frequency of the instance"""
97-
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
98-
'in a future version; use DatetimeIndex.freq instead.')
97+
msg = ('{cls}.offset has been deprecated and will be removed '
98+
'in a future version; use {cls}.freq instead.'
99+
.format(cls=type(self).__name__))
99100
warnings.warn(msg, FutureWarning, stacklevel=2)
100101
return self.freq
101102

102103
@offset.setter
103104
def offset(self, value):
104105
"""get/set the frequency of the instance"""
105-
msg = ('DatetimeIndex.offset has been deprecated and will be removed '
106-
'in a future version; use DatetimeIndex.freq instead.')
106+
msg = ('{cls}.offset has been deprecated and will be removed '
107+
'in a future version; use {cls}.freq instead.'
108+
.format(cls=type(self).__name__))
107109
warnings.warn(msg, FutureWarning, stacklevel=2)
108110
self.freq = value
109111

112+
@property # NB: override with cache_readonly in immutable subclasses
113+
def is_normalized(self):
114+
"""
115+
Returns True if all of the dates are at midnight ("no time")
116+
"""
117+
return conversion.is_date_array_normalized(self.asi8, self.tz)
118+
110119
# ----------------------------------------------------------------
111120
# Array-like Methods
112121

@@ -582,7 +591,7 @@ def date(self):
582591

583592
def to_julian_date(self):
584593
"""
585-
Convert DatetimeIndex to float64 ndarray of Julian Dates.
594+
Convert Datetime Array to float64 ndarray of Julian Dates.
586595
0 Julian date is noon January 1, 4713 BC.
587596
http://en.wikipedia.org/wiki/Julian_day
588597
"""

pandas/core/arrays/period.py

+154-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@
55
import numpy as np
66

77
from pandas._libs import lib
8-
from pandas._libs.tslib import NaT
8+
from pandas._libs.tslib import NaT, iNaT
99
from pandas._libs.tslibs.period import (
1010
Period, IncompatibleFrequency, DIFFERENT_FREQ_INDEX,
1111
get_period_field_arr)
1212
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
1313
from pandas._libs.tslibs.fields import isleapyear_arr
1414

15+
from pandas import compat
1516
from pandas.util._decorators import cache_readonly
1617

18+
from pandas.core.dtypes.common import is_integer_dtype, is_float_dtype
1719
from pandas.core.dtypes.dtypes import PeriodDtype
1820

1921
from pandas.tseries import frequencies
@@ -33,6 +35,47 @@ def f(self):
3335
return property(f)
3436

3537

38+
def _period_array_cmp(opname, cls):
39+
"""
40+
Wrap comparison operations to convert Period-like to PeriodDtype
41+
"""
42+
nat_result = True if opname == '__ne__' else False
43+
44+
def wrapper(self, other):
45+
op = getattr(self._ndarray_values, opname)
46+
if isinstance(other, Period):
47+
if other.freq != self.freq:
48+
msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
49+
raise IncompatibleFrequency(msg)
50+
51+
result = op(other.ordinal)
52+
elif isinstance(other, PeriodArrayMixin):
53+
if other.freq != self.freq:
54+
msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
55+
raise IncompatibleFrequency(msg)
56+
57+
result = op(other._ndarray_values)
58+
59+
mask = self._isnan | other._isnan
60+
if mask.any():
61+
result[mask] = nat_result
62+
63+
return result
64+
elif other is NaT:
65+
result = np.empty(len(self._ndarray_values), dtype=bool)
66+
result.fill(nat_result)
67+
else:
68+
other = Period(other, freq=self.freq)
69+
result = op(other.ordinal)
70+
71+
if self.hasnans:
72+
result[self._isnan] = nat_result
73+
74+
return result
75+
76+
return compat.set_function_name(wrapper, opname, cls)
77+
78+
3679
class PeriodArrayMixin(DatetimeLikeArrayMixin):
3780
@property
3881
def _box_func(self):
@@ -59,12 +102,62 @@ def freq(self):
59102
@freq.setter
60103
def freq(self, value):
61104
msg = ('Setting {cls}.freq has been deprecated and will be '
62-
'removed in a future version; use PeriodIndex.asfreq instead. '
105+
'removed in a future version; use {cls}.asfreq instead. '
63106
'The {cls}.freq setter is not guaranteed to work.')
64107
warnings.warn(msg.format(cls=type(self).__name__),
65108
FutureWarning, stacklevel=2)
66109
self._freq = value
67110

111+
# --------------------------------------------------------------------
112+
# Constructors
113+
114+
_attributes = ["freq"]
115+
116+
def _get_attributes_dict(self):
117+
"""return an attributes dict for my class"""
118+
return {k: getattr(self, k, None) for k in self._attributes}
119+
120+
# TODO: share docstring?
121+
def _shallow_copy(self, values=None, **kwargs):
122+
if values is None:
123+
values = self._ndarray_values
124+
attributes = self._get_attributes_dict()
125+
attributes.update(kwargs)
126+
return self._simple_new(values, **attributes)
127+
128+
@classmethod
129+
def _simple_new(cls, values, freq=None):
130+
"""
131+
Values can be any type that can be coerced to Periods.
132+
Ordinals in an ndarray are fastpath-ed to `_from_ordinals`
133+
"""
134+
if not is_integer_dtype(values):
135+
values = np.array(values, copy=False)
136+
if len(values) > 0 and is_float_dtype(values):
137+
raise TypeError("{cls} can't take floats"
138+
.format(cls=cls.__name__))
139+
return cls(values, freq=freq)
140+
141+
return cls._from_ordinals(values, freq)
142+
143+
__new__ = _simple_new # For now...
144+
145+
@classmethod
146+
def _from_ordinals(cls, values, freq=None):
147+
"""
148+
Values should be int ordinals
149+
`__new__` & `_simple_new` cooerce to ordinals and call this method
150+
"""
151+
152+
values = np.array(values, dtype='int64', copy=False)
153+
154+
result = object.__new__(cls)
155+
result._data = values
156+
if freq is None:
157+
raise ValueError('freq is not specified and cannot be inferred')
158+
result._freq = Period._maybe_convert_freq(freq)
159+
return result
160+
68161
# --------------------------------------------------------------------
69162
# Vectorized analogues of Period properties
70163

@@ -115,6 +208,52 @@ def _sub_period(self, other):
115208

116209
return new_data
117210

211+
def _add_offset(self, other):
212+
assert not isinstance(other, Tick)
213+
base = frequencies.get_base_alias(other.rule_code)
214+
if base != self.freq.rule_code:
215+
msg = DIFFERENT_FREQ_INDEX.format(self.freqstr, other.freqstr)
216+
raise IncompatibleFrequency(msg)
217+
return self.shift(other.n)
218+
219+
def _add_delta_td(self, other):
220+
assert isinstance(other, (timedelta, np.timedelta64, Tick))
221+
nanos = delta_to_nanoseconds(other)
222+
own_offset = frequencies.to_offset(self.freq.rule_code)
223+
224+
if isinstance(own_offset, Tick):
225+
offset_nanos = delta_to_nanoseconds(own_offset)
226+
if np.all(nanos % offset_nanos == 0):
227+
return self.shift(nanos // offset_nanos)
228+
229+
# raise when input doesn't have freq
230+
raise IncompatibleFrequency("Input has different freq from "
231+
"{cls}(freq={freqstr})"
232+
.format(cls=type(self).__name__,
233+
freqstr=self.freqstr))
234+
235+
def _add_delta(self, other):
236+
ordinal_delta = self._maybe_convert_timedelta(other)
237+
return self.shift(ordinal_delta)
238+
239+
def shift(self, n):
240+
"""
241+
Specialized shift which produces an Period Array/Index
242+
243+
Parameters
244+
----------
245+
n : int
246+
Periods to shift by
247+
248+
Returns
249+
-------
250+
shifted : Period Array/Index
251+
"""
252+
values = self._ndarray_values + n * self.freq.n
253+
if self.hasnans:
254+
values[self._isnan] = iNaT
255+
return self._shallow_copy(values=values)
256+
118257
def _maybe_convert_timedelta(self, other):
119258
"""
120259
Convert timedelta-like input to an integer multiple of self.freq
@@ -161,3 +300,16 @@ def _maybe_convert_timedelta(self, other):
161300
msg = "Input has different freq from {cls}(freq={freqstr})"
162301
raise IncompatibleFrequency(msg.format(cls=type(self).__name__,
163302
freqstr=self.freqstr))
303+
304+
@classmethod
305+
def _add_comparison_methods(cls):
306+
""" add in comparison methods """
307+
cls.__eq__ = _period_array_cmp('__eq__', cls)
308+
cls.__ne__ = _period_array_cmp('__ne__', cls)
309+
cls.__lt__ = _period_array_cmp('__lt__', cls)
310+
cls.__gt__ = _period_array_cmp('__gt__', cls)
311+
cls.__le__ = _period_array_cmp('__le__', cls)
312+
cls.__ge__ = _period_array_cmp('__ge__', cls)
313+
314+
315+
PeriodArrayMixin._add_comparison_methods()

pandas/core/arrays/timedelta.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def total_seconds(self):
9898
9999
Returns
100100
-------
101-
seconds : ndarray, Float64Index, or Series
101+
seconds : [ndarray, Float64Index, Series]
102102
When the calling object is a TimedeltaArray, the return type
103103
is ndarray. When the calling object is a TimedeltaIndex,
104104
the return type is a Float64Index. When the calling object

0 commit comments

Comments
 (0)