6
6
# necessary to enforce truediv in Python 2.X
7
7
from __future__ import division
8
8
import operator
9
+ import warnings
9
10
import numpy as np
10
11
import pandas as pd
11
12
from pandas import compat , lib , tslib
21
22
needs_i8_conversion , is_datetimelike_v_numeric ,
22
23
is_integer_dtype , is_categorical_dtype , is_object_dtype ,
23
24
is_timedelta64_dtype , is_datetime64_dtype , is_bool_dtype )
24
-
25
+ from pandas . io . common import PerformanceWarning
25
26
# -----------------------------------------------------------------------------
26
27
# Functions that add arithmetic methods to objects, given arithmetic factory
27
28
# methods
@@ -276,12 +277,16 @@ def __init__(self, left, right, name):
276
277
277
278
self .left = left
278
279
self .right = right
279
- lvalues = self ._convert_to_array (left , name = name )
280
- rvalues = self ._convert_to_array (right , name = name , other = lvalues )
281
280
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 )
282
285
self .is_timedelta_lhs = is_timedelta64_dtype (left )
283
286
self .is_datetime_lhs = is_datetime64_dtype (left )
284
287
self .is_integer_lhs = left .dtype .kind in ['i' , 'u' ]
288
+
289
+ rvalues = self ._convert_to_array (right , name = name , other = lvalues )
285
290
self .is_datetime_rhs = is_datetime64_dtype (rvalues )
286
291
self .is_timedelta_rhs = is_timedelta64_dtype (rvalues )
287
292
self .is_integer_rhs = rvalues .dtype .kind in ('i' , 'u' )
@@ -309,27 +314,32 @@ def _validate(self):
309
314
" passed" % self .name )
310
315
311
316
# 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 ))):
313
321
314
322
if self .name not in ('__div__' , '__truediv__' , '__add__' ,
315
323
'__sub__' ):
316
324
raise TypeError ("can only operate on a timedeltas for "
317
325
"addition, subtraction, and division, but the"
318
326
" operator [%s] was passed" % self .name )
319
327
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 )):
322
331
323
332
if self .name not in ('__add__' , '__sub__' ):
324
333
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,"
326
335
" but the operator [%s] was passed" %
327
336
self .name )
328
337
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 ):
330
340
331
341
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"
333
343
" a datetime for addition, but the operator"
334
344
" [%s] was passed" % self .name )
335
345
else :
@@ -371,18 +381,7 @@ def _convert_to_array(self, values, name=None, other=None):
371
381
elif name not in ('__truediv__' , '__div__' , '__mul__' ):
372
382
raise TypeError ("incompatible type for a datetime/timedelta "
373
383
"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' )
384
384
elif inferred_type == 'floating' :
385
-
386
385
# all nan, so ok, use the other dtype (e.g. timedelta or datetime)
387
386
if isnull (values ).all ():
388
387
values = np .empty (values .shape , dtype = other .dtype )
@@ -391,13 +390,16 @@ def _convert_to_array(self, values, name=None, other=None):
391
390
raise TypeError (
392
391
'incompatible type [{0}] for a datetime/timedelta '
393
392
'operation' .format (np .array (values ).dtype ))
393
+ elif self ._is_offset (values ):
394
+ return values
394
395
else :
395
396
raise TypeError ("incompatible type [{0}] for a datetime/timedelta"
396
397
" operation" .format (np .array (values ).dtype ))
397
398
398
399
return values
399
400
400
401
def _convert_for_datetime (self , lvalues , rvalues ):
402
+ from pandas .tseries .timedeltas import to_timedelta
401
403
mask = None
402
404
# datetimes require views
403
405
if self .is_datetime_lhs or self .is_datetime_rhs :
@@ -407,13 +409,40 @@ def _convert_for_datetime(self, lvalues, rvalues):
407
409
else :
408
410
self .dtype = 'datetime64[ns]'
409
411
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 )
412
434
413
435
# otherwise it's a timedelta
414
436
else :
415
437
self .dtype = 'timedelta64[ns]'
416
438
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
+
417
446
lvalues = lvalues .astype (np .int64 )
418
447
rvalues = rvalues .astype (np .int64 )
419
448
@@ -439,6 +468,16 @@ def f(x):
439
468
self .lvalues = lvalues
440
469
self .rvalues = rvalues
441
470
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
+
442
481
@classmethod
443
482
def maybe_convert_for_time_op (cls , left , right , name ):
444
483
"""
@@ -532,8 +571,8 @@ def wrapper(left, right, name=name):
532
571
name = name , dtype = dtype )
533
572
else :
534
573
# 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
537
576
return left ._constructor (wrap_results (na_op (lvalues , rvalues )),
538
577
index = left .index , name = left .name ,
539
578
dtype = dtype )
0 commit comments