44cimport cython
55
66import 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
810
911import numpy as np
1012cimport numpy as np
@@ -13,9 +15,11 @@ np.import_array()
1315
1416from util cimport is_string_object
1517
16- from conversion cimport tz_convert_single
1718from pandas._libs.tslib import pydt_to_i8
1819
20+ from frequencies cimport get_freq_code
21+ from conversion cimport tz_convert_single
22+
1923# ---------------------------------------------------------------------
2024# Constants
2125
@@ -79,7 +83,6 @@ _offset_to_period_map = {
7983
8084need_suffix = [' QS' , ' BQ' , ' BQS' , ' YS' , ' AS' , ' BY' , ' BA' , ' BYS' , ' BAS' ]
8185
82-
8386for __prefix in need_suffix:
8487 for _m in _MONTHS:
8588 key = ' %s -%s ' % (__prefix, _m)
@@ -105,17 +108,38 @@ def as_datetime(obj):
105108 return obj
106109
107110
108- def _is_normalized (dt ):
111+ cpdef bint _is_normalized(dt):
109112 if (dt.hour != 0 or dt.minute != 0 or dt.second != 0 or
110113 dt.microsecond != 0 or getattr (dt, ' nanosecond' , 0 ) != 0 ):
111114 return False
112115 return True
113116
114117
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+
115139# ---------------------------------------------------------------------
116140# Business Helpers
117141
118- def _get_firstbday (wkday ):
142+ cpdef int _get_firstbday(int wkday):
119143 """
120144 wkday is the result of monthrange(year, month)
121145
@@ -194,6 +218,45 @@ def _validate_business_time(t_input):
194218 else :
195219 raise ValueError (" time data must be string or datetime.time" )
196220
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+
197260# ---------------------------------------------------------------------
198261# Mixins & Singletons
199262
@@ -206,3 +269,109 @@ class ApplyTypeError(TypeError):
206269# TODO: unused. remove?
207270class CacheableOffset (object ):
208271 _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