66# necessary to enforce truediv in Python 2.X
77from __future__ import division
88import operator
9+ import warnings
910import numpy as np
1011import pandas as pd
1112from pandas import compat , lib , tslib
2122 needs_i8_conversion , is_datetimelike_v_numeric ,
2223 is_integer_dtype , is_categorical_dtype , is_object_dtype ,
2324 is_timedelta64_dtype , is_datetime64_dtype , is_bool_dtype )
24-
25+ from pandas . io . common import PerformanceWarning
2526# -----------------------------------------------------------------------------
2627# Functions that add arithmetic methods to objects, given arithmetic factory
2728# methods
@@ -276,12 +277,16 @@ def __init__(self, left, right, name):
276277
277278 self .left = left
278279 self .right = right
279- lvalues = self ._convert_to_array (left , name = name )
280- rvalues = self ._convert_to_array (right , name = name , other = lvalues )
281280
281+ self .is_offset_lhs = self ._is_offset (left )
282+ self .is_offset_rhs = self ._is_offset (right )
283+
284+ lvalues = self ._convert_to_array (left , name = name )
282285 self .is_timedelta_lhs = is_timedelta64_dtype (left )
283286 self .is_datetime_lhs = is_datetime64_dtype (left )
284287 self .is_integer_lhs = left .dtype .kind in ['i' , 'u' ]
288+
289+ rvalues = self ._convert_to_array (right , name = name , other = lvalues )
285290 self .is_datetime_rhs = is_datetime64_dtype (rvalues )
286291 self .is_timedelta_rhs = is_timedelta64_dtype (rvalues )
287292 self .is_integer_rhs = rvalues .dtype .kind in ('i' , 'u' )
@@ -309,27 +314,32 @@ def _validate(self):
309314 " passed" % self .name )
310315
311316 # 2 timedeltas
312- elif self .is_timedelta_lhs and self .is_timedelta_rhs :
317+ elif ((self .is_timedelta_lhs and
318+ (self .is_timedelta_rhs or self .is_offset_rhs )) or
319+ (self .is_timedelta_rhs and
320+ (self .is_timedelta_lhs or self .is_offset_lhs ))):
313321
314322 if self .name not in ('__div__' , '__truediv__' , '__add__' ,
315323 '__sub__' ):
316324 raise TypeError ("can only operate on a timedeltas for "
317325 "addition, subtraction, and division, but the"
318326 " operator [%s] was passed" % self .name )
319327
320- # datetime and timedelta
321- elif self .is_datetime_lhs and self .is_timedelta_rhs :
328+ # datetime and timedelta/DateOffset
329+ elif (self .is_datetime_lhs and
330+ (self .is_timedelta_rhs or self .is_offset_rhs )):
322331
323332 if self .name not in ('__add__' , '__sub__' ):
324333 raise TypeError ("can only operate on a datetime with a rhs of"
325- " a timedelta for addition and subtraction, "
334+ " a timedelta/DateOffset for addition and subtraction,"
326335 " but the operator [%s] was passed" %
327336 self .name )
328337
329- elif self .is_timedelta_lhs and self .is_datetime_rhs :
338+ elif ((self .is_timedelta_lhs or self .is_offset_lhs )
339+ and self .is_datetime_rhs ):
330340
331341 if self .name != '__add__' :
332- raise TypeError ("can only operate on a timedelta and"
342+ raise TypeError ("can only operate on a timedelta/DateOffset and"
333343 " a datetime for addition, but the operator"
334344 " [%s] was passed" % self .name )
335345 else :
@@ -371,18 +381,7 @@ def _convert_to_array(self, values, name=None, other=None):
371381 elif name not in ('__truediv__' , '__div__' , '__mul__' ):
372382 raise TypeError ("incompatible type for a datetime/timedelta "
373383 "operation [{0}]" .format (name ))
374- elif isinstance (values [0 ], pd .DateOffset ):
375- # handle DateOffsets
376- os = np .array ([getattr (v , 'delta' , None ) for v in values ])
377- mask = isnull (os )
378- if mask .any ():
379- raise TypeError ("cannot use a non-absolute DateOffset in "
380- "datetime/timedelta operations [{0}]" .format (
381- ', ' .join ([com .pprint_thing (v )
382- for v in values [mask ]])))
383- values = to_timedelta (os , errors = 'coerce' )
384384 elif inferred_type == 'floating' :
385-
386385 # all nan, so ok, use the other dtype (e.g. timedelta or datetime)
387386 if isnull (values ).all ():
388387 values = np .empty (values .shape , dtype = other .dtype )
@@ -391,13 +390,16 @@ def _convert_to_array(self, values, name=None, other=None):
391390 raise TypeError (
392391 'incompatible type [{0}] for a datetime/timedelta '
393392 'operation' .format (np .array (values ).dtype ))
393+ elif self ._is_offset (values ):
394+ return values
394395 else :
395396 raise TypeError ("incompatible type [{0}] for a datetime/timedelta"
396397 " operation" .format (np .array (values ).dtype ))
397398
398399 return values
399400
400401 def _convert_for_datetime (self , lvalues , rvalues ):
402+ from pandas .tseries .timedeltas import to_timedelta
401403 mask = None
402404 # datetimes require views
403405 if self .is_datetime_lhs or self .is_datetime_rhs :
@@ -407,13 +409,40 @@ def _convert_for_datetime(self, lvalues, rvalues):
407409 else :
408410 self .dtype = 'datetime64[ns]'
409411 mask = isnull (lvalues ) | isnull (rvalues )
410- lvalues = lvalues .view (np .int64 )
411- rvalues = rvalues .view (np .int64 )
412+
413+ # if adding single offset try vectorized path
414+ # in DatetimeIndex; otherwise elementwise apply
415+ if self .is_offset_lhs :
416+ if len (lvalues ) == 1 :
417+ rvalues = pd .DatetimeIndex (rvalues )
418+ lvalues = lvalues [0 ]
419+ else :
420+ warnings .warn ("Adding/subtracting array of DateOffsets to Series not vectorized" ,
421+ PerformanceWarning )
422+ rvalues = rvalues .astype ('O' )
423+ elif self .is_offset_rhs :
424+ if len (rvalues ) == 1 :
425+ lvalues = pd .DatetimeIndex (lvalues )
426+ rvalues = rvalues [0 ]
427+ else :
428+ warnings .warn ("Adding/subtracting array of DateOffsets to Series not vectorized" ,
429+ PerformanceWarning )
430+ lvalues = lvalues .astype ('O' )
431+ else :
432+ lvalues = lvalues .view (np .int64 )
433+ rvalues = rvalues .view (np .int64 )
412434
413435 # otherwise it's a timedelta
414436 else :
415437 self .dtype = 'timedelta64[ns]'
416438 mask = isnull (lvalues ) | isnull (rvalues )
439+
440+ # convert Tick DateOffset to underlying delta
441+ if self .is_offset_lhs :
442+ lvalues = to_timedelta (lvalues )
443+ if self .is_offset_rhs :
444+ rvalues = to_timedelta (rvalues )
445+
417446 lvalues = lvalues .astype (np .int64 )
418447 rvalues = rvalues .astype (np .int64 )
419448
@@ -439,6 +468,16 @@ def f(x):
439468 self .lvalues = lvalues
440469 self .rvalues = rvalues
441470
471+
472+ def _is_offset (self , arr_or_obj ):
473+ """ check if obj or all elements of list-like is DateOffset """
474+ if isinstance (arr_or_obj , pd .DateOffset ):
475+ return True
476+ elif is_list_like (arr_or_obj ):
477+ return all (isinstance (x , pd .DateOffset ) for x in arr_or_obj )
478+ else :
479+ return False
480+
442481 @classmethod
443482 def maybe_convert_for_time_op (cls , left , right , name ):
444483 """
@@ -532,8 +571,8 @@ def wrapper(left, right, name=name):
532571 name = name , dtype = dtype )
533572 else :
534573 # scalars
535- if hasattr (lvalues , 'values' ):
536- lvalues = lvalues .values
574+ if hasattr (lvalues , 'values' ) and not isinstance ( lvalues , pd . DatetimeIndex ) :
575+ lvalues = lvalues .values
537576 return left ._constructor (wrap_results (na_op (lvalues , rvalues )),
538577 index = left .index , name = left .name ,
539578 dtype = dtype )
0 commit comments