5
5
import numpy as np
6
6
7
7
from pandas ._libs import lib
8
- from pandas ._libs .tslib import NaT
8
+ from pandas ._libs .tslib import NaT , iNaT
9
9
from pandas ._libs .tslibs .period import (
10
10
Period , IncompatibleFrequency , DIFFERENT_FREQ_INDEX ,
11
11
get_period_field_arr )
12
12
from pandas ._libs .tslibs .timedeltas import delta_to_nanoseconds
13
13
from pandas ._libs .tslibs .fields import isleapyear_arr
14
14
15
+ from pandas import compat
15
16
from pandas .util ._decorators import cache_readonly
16
17
18
+ from pandas .core .dtypes .common import is_integer_dtype , is_float_dtype
17
19
from pandas .core .dtypes .dtypes import PeriodDtype
18
20
19
21
from pandas .tseries import frequencies
@@ -33,6 +35,47 @@ def f(self):
33
35
return property (f )
34
36
35
37
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
+
36
79
class PeriodArrayMixin (DatetimeLikeArrayMixin ):
37
80
@property
38
81
def _box_func (self ):
@@ -59,12 +102,62 @@ def freq(self):
59
102
@freq .setter
60
103
def freq (self , value ):
61
104
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. '
63
106
'The {cls}.freq setter is not guaranteed to work.' )
64
107
warnings .warn (msg .format (cls = type (self ).__name__ ),
65
108
FutureWarning , stacklevel = 2 )
66
109
self ._freq = value
67
110
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
+
68
161
# --------------------------------------------------------------------
69
162
# Vectorized analogues of Period properties
70
163
@@ -115,6 +208,52 @@ def _sub_period(self, other):
115
208
116
209
return new_data
117
210
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
+
118
257
def _maybe_convert_timedelta (self , other ):
119
258
"""
120
259
Convert timedelta-like input to an integer multiple of self.freq
@@ -161,3 +300,16 @@ def _maybe_convert_timedelta(self, other):
161
300
msg = "Input has different freq from {cls}(freq={freqstr})"
162
301
raise IncompatibleFrequency (msg .format (cls = type (self ).__name__ ,
163
302
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 ()
0 commit comments