4
4
cimport cython
5
5
6
6
import time
7
- from cpython.datetime cimport time as dt_time
7
+ from cpython.datetime cimport timedelta, time as dt_time
8
+
9
+ from dateutil.relativedelta import relativedelta
8
10
9
11
import numpy as np
10
12
cimport numpy as np
@@ -13,9 +15,11 @@ np.import_array()
13
15
14
16
from util cimport is_string_object
15
17
16
- from conversion cimport tz_convert_single
17
18
from pandas._libs.tslib import pydt_to_i8
18
19
20
+ from frequencies cimport get_freq_code
21
+ from conversion cimport tz_convert_single
22
+
19
23
# ---------------------------------------------------------------------
20
24
# Constants
21
25
@@ -79,7 +83,6 @@ _offset_to_period_map = {
79
83
80
84
need_suffix = [' QS' , ' BQ' , ' BQS' , ' YS' , ' AS' , ' BY' , ' BA' , ' BYS' , ' BAS' ]
81
85
82
-
83
86
for __prefix in need_suffix:
84
87
for _m in _MONTHS:
85
88
key = ' %s -%s ' % (__prefix, _m)
@@ -105,17 +108,38 @@ def as_datetime(obj):
105
108
return obj
106
109
107
110
108
- def _is_normalized (dt ):
111
+ cpdef bint _is_normalized(dt):
109
112
if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 or
110
113
dt.microsecond != 0 or getattr (dt, ' nanosecond' , 0 ) != 0 ):
111
114
return False
112
115
return True
113
116
114
117
118
+ def apply_index_wraps (func ):
119
+ # Note: normally we would use `@functools.wraps(func)`, but this does
120
+ # not play nicely wtih cython class methods
121
+ def wrapper (self , other ):
122
+ result = func(self , other)
123
+ if self .normalize:
124
+ result = result.to_period(' D' ).to_timestamp()
125
+ return result
126
+
127
+ # do @functools.wraps(func) manually since it doesn't work on cdef funcs
128
+ wrapper.__name__ = func.__name__
129
+ wrapper.__doc__ = func.__doc__
130
+ try :
131
+ wrapper.__module__ = func.__module__
132
+ except AttributeError :
133
+ # AttributeError: 'method_descriptor' object has no
134
+ # attribute '__module__'
135
+ pass
136
+ return wrapper
137
+
138
+
115
139
# ---------------------------------------------------------------------
116
140
# Business Helpers
117
141
118
- def _get_firstbday (wkday ):
142
+ cpdef int _get_firstbday(int wkday):
119
143
"""
120
144
wkday is the result of monthrange(year, month)
121
145
@@ -194,6 +218,45 @@ def _validate_business_time(t_input):
194
218
else :
195
219
raise ValueError (" time data must be string or datetime.time" )
196
220
221
+
222
+ # ---------------------------------------------------------------------
223
+ # Constructor Helpers
224
+
225
+ _rd_kwds = set ([
226
+ ' years' , ' months' , ' weeks' , ' days' ,
227
+ ' year' , ' month' , ' week' , ' day' , ' weekday' ,
228
+ ' hour' , ' minute' , ' second' , ' microsecond' ,
229
+ ' nanosecond' , ' nanoseconds' ,
230
+ ' hours' , ' minutes' , ' seconds' , ' milliseconds' , ' microseconds' ])
231
+
232
+
233
+ def _determine_offset (kwds ):
234
+ # timedelta is used for sub-daily plural offsets and all singular
235
+ # offsets relativedelta is used for plural offsets of daily length or
236
+ # more nanosecond(s) are handled by apply_wraps
237
+ kwds_no_nanos = dict (
238
+ (k, v) for k, v in kwds.items()
239
+ if k not in (' nanosecond' , ' nanoseconds' )
240
+ )
241
+ # TODO: Are nanosecond and nanoseconds allowed somewhere?
242
+
243
+ _kwds_use_relativedelta = (' years' , ' months' , ' weeks' , ' days' ,
244
+ ' year' , ' month' , ' week' , ' day' , ' weekday' ,
245
+ ' hour' , ' minute' , ' second' , ' microsecond' )
246
+
247
+ use_relativedelta = False
248
+ if len (kwds_no_nanos) > 0 :
249
+ if any (k in _kwds_use_relativedelta for k in kwds_no_nanos):
250
+ offset = relativedelta(** kwds_no_nanos)
251
+ use_relativedelta = True
252
+ else :
253
+ # sub-daily offset - use timedelta (tz-aware)
254
+ offset = timedelta(** kwds_no_nanos)
255
+ else :
256
+ offset = timedelta(1 )
257
+ return offset, use_relativedelta
258
+
259
+
197
260
# ---------------------------------------------------------------------
198
261
# Mixins & Singletons
199
262
@@ -206,3 +269,109 @@ class ApplyTypeError(TypeError):
206
269
# TODO: unused. remove?
207
270
class CacheableOffset (object ):
208
271
_cacheable = True
272
+
273
+
274
+ class BeginMixin (object ):
275
+ # helper for vectorized offsets
276
+
277
+ def _beg_apply_index (self , i , freq ):
278
+ """ Offsets index to beginning of Period frequency"""
279
+
280
+ off = i.to_perioddelta(' D' )
281
+
282
+ base, mult = get_freq_code(freq)
283
+ base_period = i.to_period(base)
284
+ if self .n <= 0 :
285
+ # when subtracting, dates on start roll to prior
286
+ roll = np.where(base_period.to_timestamp() == i - off,
287
+ self .n, self .n + 1 )
288
+ else :
289
+ roll = self .n
290
+
291
+ base = (base_period + roll).to_timestamp()
292
+ return base + off
293
+
294
+
295
+ class EndMixin (object ):
296
+ # helper for vectorized offsets
297
+
298
+ def _end_apply_index (self , i , freq ):
299
+ """ Offsets index to end of Period frequency"""
300
+
301
+ off = i.to_perioddelta(' D' )
302
+
303
+ base, mult = get_freq_code(freq)
304
+ base_period = i.to_period(base)
305
+ if self .n > 0 :
306
+ # when adding, dates on end roll to next
307
+ roll = np.where(base_period.to_timestamp(how = ' end' ) == i - off,
308
+ self .n, self .n - 1 )
309
+ else :
310
+ roll = self .n
311
+
312
+ base = (base_period + roll).to_timestamp(how = ' end' )
313
+ return base + off
314
+
315
+
316
+ # ---------------------------------------------------------------------
317
+ # Base Classes
318
+
319
+ class _BaseOffset (object ):
320
+ """
321
+ Base class for DateOffset methods that are not overriden by subclasses
322
+ and will (after pickle errors are resolved) go into a cdef class.
323
+ """
324
+ _typ = " dateoffset"
325
+ _normalize_cache = True
326
+ _cacheable = False
327
+
328
+ def __call__ (self , other ):
329
+ return self .apply(other)
330
+
331
+ def __mul__ (self , someInt ):
332
+ return self .__class__ (n = someInt * self .n, normalize = self .normalize,
333
+ ** self .kwds)
334
+
335
+ def __neg__ (self ):
336
+ # Note: we are defering directly to __mul__ instead of __rmul__, as
337
+ # that allows us to use methods that can go in a `cdef class`
338
+ return self * - 1
339
+
340
+ def copy (self ):
341
+ # Note: we are defering directly to __mul__ instead of __rmul__, as
342
+ # that allows us to use methods that can go in a `cdef class`
343
+ return self * 1
344
+
345
+ # TODO: this is never true. fix it or get rid of it
346
+ def _should_cache (self ):
347
+ return self .isAnchored() and self ._cacheable
348
+
349
+ def __repr__ (self ):
350
+ className = getattr (self , ' _outputName' , type (self ).__name__)
351
+
352
+ if abs (self .n) != 1 :
353
+ plural = ' s'
354
+ else :
355
+ plural = ' '
356
+
357
+ n_str = " "
358
+ if self .n != 1 :
359
+ n_str = " %s * " % self .n
360
+
361
+ out = ' <%s ' % n_str + className + plural + self ._repr_attrs() + ' >'
362
+ return out
363
+
364
+
365
+ class BaseOffset (_BaseOffset ):
366
+ # Here we add __rfoo__ methods that don't play well with cdef classes
367
+ def __rmul__ (self , someInt ):
368
+ return self .__mul__ (someInt)
369
+
370
+ def __radd__ (self , other ):
371
+ return self .__add__ (other)
372
+
373
+ def __rsub__ (self , other ):
374
+ if getattr (other, ' _typ' , None ) in [' datetimeindex' , ' series' ]:
375
+ # i.e. isinstance(other, (ABCDatetimeIndex, ABCSeries))
376
+ return other - self
377
+ return - self + other
0 commit comments