11# -*- coding: utf-8 -*-
22from datetime import timedelta
3+ import operator
34import warnings
45
56import numpy as np
1718from pandas .util ._decorators import (cache_readonly , deprecate_kwarg )
1819
1920from pandas .core .dtypes .common import (
20- is_integer_dtype , is_float_dtype , is_period_dtype ,
21- is_datetime64_dtype )
21+ is_integer_dtype , is_float_dtype , is_period_dtype , is_timedelta64_dtype ,
22+ is_datetime64_dtype , _TD_DTYPE )
2223from pandas .core .dtypes .dtypes import PeriodDtype
2324from pandas .core .dtypes .generic import ABCSeries
2425
@@ -362,24 +363,54 @@ def _add_offset(self, other):
362363 return self ._time_shift (other .n )
363364
364365 def _add_delta_td (self , other ):
366+ assert isinstance (self .freq , Tick ) # checked by calling function
365367 assert isinstance (other , (timedelta , np .timedelta64 , Tick ))
366- nanos = delta_to_nanoseconds (other )
367- own_offset = frequencies .to_offset (self .freq .rule_code )
368368
369- if isinstance (own_offset , Tick ):
370- offset_nanos = delta_to_nanoseconds (own_offset )
371- if np .all (nanos % offset_nanos == 0 ):
372- return self ._time_shift (nanos // offset_nanos )
369+ delta = self ._check_timedeltalike_freq_compat (other )
373370
374- # raise when input doesn't have freq
375- raise IncompatibleFrequency ("Input has different freq from "
376- "{cls}(freq={freqstr})"
377- .format (cls = type (self ).__name__ ,
378- freqstr = self .freqstr ))
371+ # Note: when calling parent class's _add_delta_td, it will call
372+ # delta_to_nanoseconds(delta). Because delta here is an integer,
373+ # delta_to_nanoseconds will return it unchanged.
374+ return DatetimeLikeArrayMixin ._add_delta_td (self , delta )
375+
376+ def _add_delta_tdi (self , other ):
377+ assert isinstance (self .freq , Tick ) # checked by calling function
378+
379+ delta = self ._check_timedeltalike_freq_compat (other )
380+ return self ._addsub_int_array (delta , operator .add )
379381
380382 def _add_delta (self , other ):
381- ordinal_delta = self ._maybe_convert_timedelta (other )
382- return self ._time_shift (ordinal_delta )
383+ """
384+ Add a timedelta-like, Tick, or TimedeltaIndex-like object
385+ to self.
386+
387+ Parameters
388+ ----------
389+ other : {timedelta, np.timedelta64, Tick,
390+ TimedeltaIndex, ndarray[timedelta64]}
391+
392+ Returns
393+ -------
394+ result : same type as self
395+ """
396+ if not isinstance (self .freq , Tick ):
397+ # We cannot add timedelta-like to non-tick PeriodArray
398+ raise IncompatibleFrequency ("Input has different freq from "
399+ "{cls}(freq={freqstr})"
400+ .format (cls = type (self ).__name__ ,
401+ freqstr = self .freqstr ))
402+
403+ # TODO: standardize across datetimelike subclasses whether to return
404+ # i8 view or _shallow_copy
405+ if isinstance (other , (Tick , timedelta , np .timedelta64 )):
406+ new_values = self ._add_delta_td (other )
407+ return self ._shallow_copy (new_values )
408+ elif is_timedelta64_dtype (other ):
409+ # ndarray[timedelta64] or TimedeltaArray/index
410+ new_values = self ._add_delta_tdi (other )
411+ return self ._shallow_copy (new_values )
412+ else : # pragma: no cover
413+ raise TypeError (type (other ).__name__ )
383414
384415 @deprecate_kwarg (old_arg_name = 'n' , new_arg_name = 'periods' )
385416 def shift (self , periods ):
@@ -435,14 +466,9 @@ def _maybe_convert_timedelta(self, other):
435466 other , (timedelta , np .timedelta64 , Tick , np .ndarray )):
436467 offset = frequencies .to_offset (self .freq .rule_code )
437468 if isinstance (offset , Tick ):
438- if isinstance (other , np .ndarray ):
439- nanos = np .vectorize (delta_to_nanoseconds )(other )
440- else :
441- nanos = delta_to_nanoseconds (other )
442- offset_nanos = delta_to_nanoseconds (offset )
443- check = np .all (nanos % offset_nanos == 0 )
444- if check :
445- return nanos // offset_nanos
469+ # _check_timedeltalike_freq_compat will raise if incompatible
470+ delta = self ._check_timedeltalike_freq_compat (other )
471+ return delta
446472 elif isinstance (other , DateOffset ):
447473 freqstr = other .rule_code
448474 base = frequencies .get_base_alias (freqstr )
@@ -461,6 +487,58 @@ def _maybe_convert_timedelta(self, other):
461487 raise IncompatibleFrequency (msg .format (cls = type (self ).__name__ ,
462488 freqstr = self .freqstr ))
463489
490+ def _check_timedeltalike_freq_compat (self , other ):
491+ """
492+ Arithmetic operations with timedelta-like scalars or array `other`
493+ are only valid if `other` is an integer multiple of `self.freq`.
494+ If the operation is valid, find that integer multiple. Otherwise,
495+ raise because the operation is invalid.
496+
497+ Parameters
498+ ----------
499+ other : timedelta, np.timedelta64, Tick,
500+ ndarray[timedelta64], TimedeltaArray, TimedeltaIndex
501+
502+ Returns
503+ -------
504+ multiple : int or ndarray[int64]
505+
506+ Raises
507+ ------
508+ IncompatibleFrequency
509+ """
510+ assert isinstance (self .freq , Tick ) # checked by calling function
511+ own_offset = frequencies .to_offset (self .freq .rule_code )
512+ base_nanos = delta_to_nanoseconds (own_offset )
513+
514+ if isinstance (other , (timedelta , np .timedelta64 , Tick )):
515+ nanos = delta_to_nanoseconds (other )
516+
517+ elif isinstance (other , np .ndarray ):
518+ # numpy timedelta64 array; all entries must be compatible
519+ assert other .dtype .kind == 'm'
520+ if other .dtype != _TD_DTYPE :
521+ # i.e. non-nano unit
522+ # TODO: disallow unit-less timedelta64
523+ other = other .astype (_TD_DTYPE )
524+ nanos = other .view ('i8' )
525+ else :
526+ # TimedeltaArray/Index
527+ nanos = other .asi8
528+
529+ if np .all (nanos % base_nanos == 0 ):
530+ # nanos being added is an integer multiple of the
531+ # base-frequency to self.freq
532+ delta = nanos // base_nanos
533+ # delta is the integer (or integer-array) number of periods
534+ # by which will be added to self.
535+ return delta
536+
537+ raise IncompatibleFrequency ("Input has different freq from "
538+ "{cls}(freq={freqstr})"
539+ .format (cls = type (self ).__name__ ,
540+ freqstr = self .freqstr ))
541+
464542
465543PeriodArrayMixin ._add_comparison_ops ()
466544PeriodArrayMixin ._add_datetimelike_methods ()
0 commit comments