Skip to content

Commit d05e8f2

Browse files
jbrockmendeljreback
authored andcommitted
Move most remaining arith helpers (#21815)
1 parent 8e51fd3 commit d05e8f2

File tree

9 files changed

+310
-211
lines changed

9 files changed

+310
-211
lines changed

pandas/core/arrays/datetimelike.py

+65-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
import operator
3+
import warnings
34

45
import numpy as np
56

@@ -8,12 +9,16 @@
89
from pandas._libs.tslibs.period import (
910
DIFFERENT_FREQ_INDEX, IncompatibleFrequency)
1011

11-
from pandas.errors import NullFrequencyError
12+
from pandas.errors import NullFrequencyError, PerformanceWarning
1213

1314
from pandas.tseries import frequencies
1415
from pandas.tseries.offsets import Tick
1516

16-
from pandas.core.dtypes.common import is_period_dtype, is_timedelta64_dtype
17+
from pandas.core.dtypes.common import (
18+
is_period_dtype,
19+
is_timedelta64_dtype,
20+
is_object_dtype)
21+
1722
import pandas.core.common as com
1823
from pandas.core.algorithms import checked_add_with_arr
1924

@@ -108,38 +113,43 @@ def __getitem__(self, key):
108113
if is_int:
109114
val = getitem(key)
110115
return self._box_func(val)
116+
117+
if com.is_bool_indexer(key):
118+
key = np.asarray(key)
119+
if key.all():
120+
key = slice(0, None, None)
121+
else:
122+
key = lib.maybe_booleans_to_slice(key.view(np.uint8))
123+
124+
attribs = self._get_attributes_dict()
125+
126+
is_period = is_period_dtype(self)
127+
if is_period:
128+
freq = self.freq
111129
else:
112-
if com.is_bool_indexer(key):
113-
key = np.asarray(key)
114-
if key.all():
115-
key = slice(0, None, None)
130+
freq = None
131+
if isinstance(key, slice):
132+
if self.freq is not None and key.step is not None:
133+
freq = key.step * self.freq
116134
else:
117-
key = lib.maybe_booleans_to_slice(key.view(np.uint8))
135+
freq = self.freq
118136

119-
attribs = self._get_attributes_dict()
137+
attribs['freq'] = freq
120138

121-
is_period = is_period_dtype(self)
139+
result = getitem(key)
140+
if result.ndim > 1:
141+
# To support MPL which performs slicing with 2 dim
142+
# even though it only has 1 dim by definition
122143
if is_period:
123-
freq = self.freq
124-
else:
125-
freq = None
126-
if isinstance(key, slice):
127-
if self.freq is not None and key.step is not None:
128-
freq = key.step * self.freq
129-
else:
130-
freq = self.freq
131-
132-
attribs['freq'] = freq
144+
return self._simple_new(result, **attribs)
145+
return result
133146

134-
result = getitem(key)
135-
if result.ndim > 1:
136-
# To support MPL which performs slicing with 2 dim
137-
# even though it only has 1 dim by definition
138-
if is_period:
139-
return self._simple_new(result, **attribs)
140-
return result
147+
return self._simple_new(result, **attribs)
141148

142-
return self._simple_new(result, **attribs)
149+
def astype(self, dtype, copy=True):
150+
if is_object_dtype(dtype):
151+
return self._box_values(self.asi8)
152+
return super(DatetimeLikeArrayMixin, self).astype(dtype, copy)
143153

144154
# ------------------------------------------------------------------
145155
# Null Handling
@@ -397,3 +407,31 @@ def _addsub_int_array(self, other, op):
397407
# to _addsub_offset_array
398408
assert not is_timedelta64_dtype(self)
399409
return op(self, np.array(other) * self.freq)
410+
411+
def _addsub_offset_array(self, other, op):
412+
"""
413+
Add or subtract array-like of DateOffset objects
414+
415+
Parameters
416+
----------
417+
other : Index, np.ndarray
418+
object-dtype containing pd.DateOffset objects
419+
op : {operator.add, operator.sub}
420+
421+
Returns
422+
-------
423+
result : same class as self
424+
"""
425+
assert op in [operator.add, operator.sub]
426+
if len(other) == 1:
427+
return op(self, other[0])
428+
429+
warnings.warn("Adding/subtracting array of DateOffsets to "
430+
"{cls} not vectorized"
431+
.format(cls=type(self).__name__), PerformanceWarning)
432+
433+
res_values = op(self.astype('O').values, np.array(other))
434+
kwargs = {}
435+
if not is_period_dtype(self):
436+
kwargs['freq'] = 'infer'
437+
return type(self)(res_values, **kwargs)

pandas/core/arrays/datetimes.py

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

45
import numpy as np
@@ -11,15 +12,18 @@
1112
resolution as libresolution)
1213

1314
from pandas.util._decorators import cache_readonly
15+
from pandas.errors import PerformanceWarning
1416

1517
from pandas.core.dtypes.common import (
1618
_NS_DTYPE,
1719
is_datetime64tz_dtype,
1820
is_datetime64_dtype,
21+
is_timedelta64_dtype,
1922
_ensure_int64)
2023
from pandas.core.dtypes.dtypes import DatetimeTZDtype
2124

2225
from pandas.tseries.frequencies import to_offset, DateOffset
26+
from pandas.tseries.offsets import Tick
2327

2428
from .datetimelike import DatetimeLikeArrayMixin
2529

@@ -104,6 +108,10 @@ def _simple_new(cls, values, freq=None, tz=None, **kwargs):
104108
return result
105109

106110
def __new__(cls, values, freq=None, tz=None):
111+
if tz is None and hasattr(values, 'tz'):
112+
# e.g. DatetimeIndex
113+
tz = values.tz
114+
107115
if (freq is not None and not isinstance(freq, DateOffset) and
108116
freq != 'infer'):
109117
freq = to_offset(freq)
@@ -131,6 +139,17 @@ def dtype(self):
131139
return _NS_DTYPE
132140
return DatetimeTZDtype('ns', self.tz)
133141

142+
@property
143+
def tz(self):
144+
# GH 18595
145+
return self._tz
146+
147+
@tz.setter
148+
def tz(self, value):
149+
# GH 3746: Prevent localizing or converting the index by setting tz
150+
raise AttributeError("Cannot directly set timezone. Use tz_localize() "
151+
"or tz_convert() as appropriate")
152+
134153
@property
135154
def tzinfo(self):
136155
"""
@@ -244,6 +263,60 @@ def _sub_datelike_dti(self, other):
244263
new_values[mask] = iNaT
245264
return new_values.view('timedelta64[ns]')
246265

266+
def _add_offset(self, offset):
267+
assert not isinstance(offset, Tick)
268+
try:
269+
if self.tz is not None:
270+
values = self.tz_localize(None)
271+
else:
272+
values = self
273+
result = offset.apply_index(values)
274+
if self.tz is not None:
275+
result = result.tz_localize(self.tz)
276+
277+
except NotImplementedError:
278+
warnings.warn("Non-vectorized DateOffset being applied to Series "
279+
"or DatetimeIndex", PerformanceWarning)
280+
result = self.astype('O') + offset
281+
282+
return type(self)(result, freq='infer')
283+
284+
def _add_delta(self, delta):
285+
"""
286+
Add a timedelta-like, DateOffset, or TimedeltaIndex-like object
287+
to self.
288+
289+
Parameters
290+
----------
291+
delta : {timedelta, np.timedelta64, DateOffset,
292+
TimedelaIndex, ndarray[timedelta64]}
293+
294+
Returns
295+
-------
296+
result : same type as self
297+
298+
Notes
299+
-----
300+
The result's name is set outside of _add_delta by the calling
301+
method (__add__ or __sub__)
302+
"""
303+
from pandas.core.arrays.timedelta import TimedeltaArrayMixin
304+
305+
if isinstance(delta, (Tick, timedelta, np.timedelta64)):
306+
new_values = self._add_delta_td(delta)
307+
elif is_timedelta64_dtype(delta):
308+
if not isinstance(delta, TimedeltaArrayMixin):
309+
delta = TimedeltaArrayMixin(delta)
310+
new_values = self._add_delta_tdi(delta)
311+
else:
312+
new_values = self.astype('O') + delta
313+
314+
tz = 'UTC' if self.tz is not None else None
315+
result = type(self)(new_values, tz=tz, freq='infer')
316+
if self.tz is not None and self.tz is not utc:
317+
result = result.tz_convert(self.tz)
318+
return result
319+
247320
# -----------------------------------------------------------------
248321
# Timezone Conversion and Localization Methods
249322

pandas/core/arrays/period.py

+74-4
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,16 @@
88
from pandas._libs.tslib import NaT, iNaT
99
from pandas._libs.tslibs.period import (
1010
Period, IncompatibleFrequency, DIFFERENT_FREQ_INDEX,
11-
get_period_field_arr)
11+
get_period_field_arr, period_asfreq_arr)
12+
from pandas._libs.tslibs import period as libperiod
1213
from pandas._libs.tslibs.timedeltas import delta_to_nanoseconds
1314
from pandas._libs.tslibs.fields import isleapyear_arr
1415

1516
from pandas import compat
1617
from pandas.util._decorators import cache_readonly
1718

18-
from pandas.core.dtypes.common import is_integer_dtype, is_float_dtype
19+
from pandas.core.dtypes.common import (
20+
is_integer_dtype, is_float_dtype, is_period_dtype)
1921
from pandas.core.dtypes.dtypes import PeriodDtype
2022

2123
from pandas.tseries import frequencies
@@ -113,12 +115,23 @@ def freq(self, value):
113115

114116
_attributes = ["freq"]
115117

118+
def __new__(cls, values, freq=None, **kwargs):
119+
if is_period_dtype(values):
120+
# PeriodArray, PeriodIndex
121+
if freq is not None and values.freq != freq:
122+
raise IncompatibleFrequency(freq, values.freq)
123+
freq = values.freq
124+
values = values.asi8
125+
126+
return cls._simple_new(values, freq, **kwargs)
127+
116128
@classmethod
117129
def _simple_new(cls, values, freq=None, **kwargs):
118130
"""
119131
Values can be any type that can be coerced to Periods.
120132
Ordinals in an ndarray are fastpath-ed to `_from_ordinals`
121133
"""
134+
122135
if not is_integer_dtype(values):
123136
values = np.array(values, copy=False)
124137
if len(values) > 0 and is_float_dtype(values):
@@ -128,8 +141,6 @@ def _simple_new(cls, values, freq=None, **kwargs):
128141

129142
return cls._from_ordinals(values, freq)
130143

131-
__new__ = _simple_new # For now...
132-
133144
@classmethod
134145
def _from_ordinals(cls, values, freq=None):
135146
"""
@@ -173,6 +184,65 @@ def is_leap_year(self):
173184
""" Logical indicating if the date belongs to a leap year """
174185
return isleapyear_arr(np.asarray(self.year))
175186

187+
def asfreq(self, freq=None, how='E'):
188+
"""
189+
Convert the Period Array/Index to the specified frequency `freq`.
190+
191+
Parameters
192+
----------
193+
freq : str
194+
a frequency
195+
how : str {'E', 'S'}
196+
'E', 'END', or 'FINISH' for end,
197+
'S', 'START', or 'BEGIN' for start.
198+
Whether the elements should be aligned to the end
199+
or start within pa period. January 31st ('END') vs.
200+
January 1st ('START') for example.
201+
202+
Returns
203+
-------
204+
new : Period Array/Index with the new frequency
205+
206+
Examples
207+
--------
208+
>>> pidx = pd.period_range('2010-01-01', '2015-01-01', freq='A')
209+
>>> pidx
210+
<class 'pandas.core.indexes.period.PeriodIndex'>
211+
[2010, ..., 2015]
212+
Length: 6, Freq: A-DEC
213+
214+
>>> pidx.asfreq('M')
215+
<class 'pandas.core.indexes.period.PeriodIndex'>
216+
[2010-12, ..., 2015-12]
217+
Length: 6, Freq: M
218+
219+
>>> pidx.asfreq('M', how='S')
220+
<class 'pandas.core.indexes.period.PeriodIndex'>
221+
[2010-01, ..., 2015-01]
222+
Length: 6, Freq: M
223+
"""
224+
how = libperiod._validate_end_alias(how)
225+
226+
freq = Period._maybe_convert_freq(freq)
227+
228+
base1, mult1 = frequencies.get_freq_code(self.freq)
229+
base2, mult2 = frequencies.get_freq_code(freq)
230+
231+
asi8 = self.asi8
232+
# mult1 can't be negative or 0
233+
end = how == 'E'
234+
if end:
235+
ordinal = asi8 + mult1 - 1
236+
else:
237+
ordinal = asi8
238+
239+
new_data = period_asfreq_arr(ordinal, base1, base2, end)
240+
241+
if self.hasnans:
242+
new_data[self._isnan] = iNaT
243+
244+
return self._simple_new(new_data, self.name, freq=freq)
245+
176246
# ------------------------------------------------------------------
177247
# Arithmetic Methods
178248

0 commit comments

Comments
 (0)