Skip to content

Commit 61c6b30

Browse files
jbrockmendelPingviinituutti
authored andcommitted
Implement mul, floordiv, mod, divmod, and reversed directly in TimedeltaArray (pandas-dev#23885)
1 parent 1235666 commit 61c6b30

File tree

7 files changed

+298
-78
lines changed

7 files changed

+298
-78
lines changed

pandas/core/arrays/timedeltas.py

+191-53
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from __future__ import division
33

44
from datetime import timedelta
5-
import operator
65
import warnings
76

87
import numpy as np
@@ -17,13 +16,12 @@
1716

1817
from pandas.core.dtypes.common import (
1918
_TD_DTYPE, ensure_int64, is_datetime64_dtype, is_float_dtype,
20-
is_integer_dtype, is_list_like, is_object_dtype, is_string_dtype,
21-
is_timedelta64_dtype)
19+
is_integer_dtype, is_list_like, is_object_dtype, is_scalar,
20+
is_string_dtype, is_timedelta64_dtype)
2221
from pandas.core.dtypes.generic import (
2322
ABCDataFrame, ABCIndexClass, ABCSeries, ABCTimedeltaIndex)
2423
from pandas.core.dtypes.missing import isna
2524

26-
from pandas.core import ops
2725
from pandas.core.algorithms import checked_add_with_arr, unique1d
2826
import pandas.core.common as com
2927

@@ -106,29 +104,6 @@ def wrapper(self, other):
106104
return compat.set_function_name(wrapper, opname, cls)
107105

108106

109-
def _wrap_tdi_op(op):
110-
"""
111-
Instead of re-implementing multiplication/division etc operations
112-
in the Array class, for now we dispatch to the TimedeltaIndex
113-
implementations.
114-
"""
115-
# TODO: implement directly here and wrap in TimedeltaIndex, instead of
116-
# the other way around
117-
def method(self, other):
118-
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
119-
return NotImplemented
120-
121-
from pandas import TimedeltaIndex
122-
obj = TimedeltaIndex(self)
123-
result = op(obj, other)
124-
if is_timedelta64_dtype(result):
125-
return type(self)(result)
126-
return np.array(result)
127-
128-
method.__name__ = '__{name}__'.format(name=op.__name__)
129-
return method
130-
131-
132107
class TimedeltaArrayMixin(dtl.DatetimeLikeArrayMixin, dtl.TimelikeOps):
133108
_typ = "timedeltaarray"
134109
__array_priority__ = 1000
@@ -332,37 +307,41 @@ def _addsub_offset_array(self, other, op):
332307
raise TypeError("Cannot add/subtract non-tick DateOffset to {cls}"
333308
.format(cls=type(self).__name__))
334309

335-
def _evaluate_with_timedelta_like(self, other, op):
336-
if isinstance(other, ABCSeries):
337-
# GH#19042
310+
def __mul__(self, other):
311+
other = lib.item_from_zerodim(other)
312+
313+
if isinstance(other, (ABCDataFrame, ABCSeries, ABCIndexClass)):
338314
return NotImplemented
339315

340-
opstr = '__{opname}__'.format(opname=op.__name__).replace('__r', '__')
341-
# allow division by a timedelta
342-
if opstr in ['__div__', '__truediv__', '__floordiv__']:
343-
if _is_convertible_to_td(other):
344-
other = Timedelta(other)
345-
if isna(other):
346-
raise NotImplementedError(
347-
"division by pd.NaT not implemented")
348-
349-
i8 = self.asi8
350-
left, right = i8, other.value
351-
352-
if opstr in ['__floordiv__']:
353-
result = op(left, right)
354-
else:
355-
result = op(left, np.float64(right))
356-
result = self._maybe_mask_results(result, fill_value=None,
357-
convert='float64')
358-
return result
316+
if is_scalar(other):
317+
# numpy will accept float and int, raise TypeError for others
318+
result = self._data * other
319+
freq = None
320+
if self.freq is not None and not isna(other):
321+
freq = self.freq * other
322+
return type(self)(result, freq=freq)
323+
324+
if not hasattr(other, "dtype"):
325+
# list, tuple
326+
other = np.array(other)
327+
if len(other) != len(self) and not is_timedelta64_dtype(other):
328+
# Exclude timedelta64 here so we correctly raise TypeError
329+
# for that instead of ValueError
330+
raise ValueError("Cannot multiply with unequal lengths")
331+
332+
if is_object_dtype(other):
333+
# this multiplication will succeed only if all elements of other
334+
# are int or float scalars, so we will end up with
335+
# timedelta64[ns]-dtyped result
336+
result = [self[n] * other[n] for n in range(len(self))]
337+
result = np.array(result)
338+
return type(self)(result)
359339

360-
return NotImplemented
340+
# numpy will accept float or int dtype, raise TypeError for others
341+
result = self._data * other
342+
return type(self)(result)
361343

362-
__mul__ = _wrap_tdi_op(operator.mul)
363344
__rmul__ = __mul__
364-
__floordiv__ = _wrap_tdi_op(operator.floordiv)
365-
__rfloordiv__ = _wrap_tdi_op(ops.rfloordiv)
366345

367346
def __truediv__(self, other):
368347
# timedelta / X is well-defined for timedelta-like or numeric X
@@ -464,6 +443,165 @@ def __rtruediv__(self, other):
464443
__div__ = __truediv__
465444
__rdiv__ = __rtruediv__
466445

446+
def __floordiv__(self, other):
447+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
448+
return NotImplemented
449+
450+
other = lib.item_from_zerodim(other)
451+
if is_scalar(other):
452+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
453+
other = Timedelta(other)
454+
if other is NaT:
455+
# treat this specifically as timedelta-NaT
456+
result = np.empty(self.shape, dtype=np.float64)
457+
result.fill(np.nan)
458+
return result
459+
460+
# dispatch to Timedelta implementation
461+
result = other.__rfloordiv__(self._data)
462+
return result
463+
464+
# at this point we should only have numeric scalars; anything
465+
# else will raise
466+
result = self.asi8 // other
467+
result[self._isnan] = iNaT
468+
freq = None
469+
if self.freq is not None:
470+
# Note: freq gets division, not floor-division
471+
freq = self.freq / other
472+
return type(self)(result.view('m8[ns]'), freq=freq)
473+
474+
if not hasattr(other, "dtype"):
475+
# list, tuple
476+
other = np.array(other)
477+
if len(other) != len(self):
478+
raise ValueError("Cannot divide with unequal lengths")
479+
480+
elif is_timedelta64_dtype(other):
481+
other = type(self)(other)
482+
483+
# numpy timedelta64 does not natively support floordiv, so operate
484+
# on the i8 values
485+
result = self.asi8 // other.asi8
486+
mask = self._isnan | other._isnan
487+
if mask.any():
488+
result = result.astype(np.int64)
489+
result[mask] = np.nan
490+
return result
491+
492+
elif is_object_dtype(other):
493+
result = [self[n] // other[n] for n in range(len(self))]
494+
result = np.array(result)
495+
if lib.infer_dtype(result) == 'timedelta':
496+
result, _ = sequence_to_td64ns(result)
497+
return type(self)(result)
498+
return result
499+
500+
elif is_integer_dtype(other) or is_float_dtype(other):
501+
result = self._data // other
502+
return type(self)(result)
503+
504+
else:
505+
dtype = getattr(other, "dtype", type(other).__name__)
506+
raise TypeError("Cannot divide {typ} by {cls}"
507+
.format(typ=dtype, cls=type(self).__name__))
508+
509+
def __rfloordiv__(self, other):
510+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
511+
return NotImplemented
512+
513+
other = lib.item_from_zerodim(other)
514+
if is_scalar(other):
515+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
516+
other = Timedelta(other)
517+
if other is NaT:
518+
# treat this specifically as timedelta-NaT
519+
result = np.empty(self.shape, dtype=np.float64)
520+
result.fill(np.nan)
521+
return result
522+
523+
# dispatch to Timedelta implementation
524+
result = other.__floordiv__(self._data)
525+
return result
526+
527+
raise TypeError("Cannot divide {typ} by {cls}"
528+
.format(typ=type(other).__name__,
529+
cls=type(self).__name__))
530+
531+
if not hasattr(other, "dtype"):
532+
# list, tuple
533+
other = np.array(other)
534+
if len(other) != len(self):
535+
raise ValueError("Cannot divide with unequal lengths")
536+
537+
elif is_timedelta64_dtype(other):
538+
other = type(self)(other)
539+
540+
# numpy timedelta64 does not natively support floordiv, so operate
541+
# on the i8 values
542+
result = other.asi8 // self.asi8
543+
mask = self._isnan | other._isnan
544+
if mask.any():
545+
result = result.astype(np.int64)
546+
result[mask] = np.nan
547+
return result
548+
549+
elif is_object_dtype(other):
550+
result = [other[n] // self[n] for n in range(len(self))]
551+
result = np.array(result)
552+
return result
553+
554+
else:
555+
dtype = getattr(other, "dtype", type(other).__name__)
556+
raise TypeError("Cannot divide {typ} by {cls}"
557+
.format(typ=dtype, cls=type(self).__name__))
558+
559+
def __mod__(self, other):
560+
# Note: This is a naive implementation, can likely be optimized
561+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
562+
return NotImplemented
563+
564+
other = lib.item_from_zerodim(other)
565+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
566+
other = Timedelta(other)
567+
return self - (self // other) * other
568+
569+
def __rmod__(self, other):
570+
# Note: This is a naive implementation, can likely be optimized
571+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
572+
return NotImplemented
573+
574+
other = lib.item_from_zerodim(other)
575+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
576+
other = Timedelta(other)
577+
return other - (other // self) * self
578+
579+
def __divmod__(self, other):
580+
# Note: This is a naive implementation, can likely be optimized
581+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
582+
return NotImplemented
583+
584+
other = lib.item_from_zerodim(other)
585+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
586+
other = Timedelta(other)
587+
588+
res1 = self // other
589+
res2 = self - res1 * other
590+
return res1, res2
591+
592+
def __rdivmod__(self, other):
593+
# Note: This is a naive implementation, can likely be optimized
594+
if isinstance(other, (ABCSeries, ABCDataFrame, ABCIndexClass)):
595+
return NotImplemented
596+
597+
other = lib.item_from_zerodim(other)
598+
if isinstance(other, (timedelta, np.timedelta64, Tick)):
599+
other = Timedelta(other)
600+
601+
res1 = other // self
602+
res2 = other - res1 * self
603+
return res1, res2
604+
467605
# Note: TimedeltaIndex overrides this in call to cls._add_numeric_methods
468606
def __neg__(self):
469607
if self.freq is not None:

pandas/core/indexes/base.py

+10-11
Original file line numberDiff line numberDiff line change
@@ -5031,23 +5031,22 @@ def _add_numeric_methods_binary(cls):
50315031
cls.__radd__ = _make_arithmetic_op(ops.radd, cls)
50325032
cls.__sub__ = _make_arithmetic_op(operator.sub, cls)
50335033
cls.__rsub__ = _make_arithmetic_op(ops.rsub, cls)
5034-
cls.__mul__ = _make_arithmetic_op(operator.mul, cls)
5035-
cls.__rmul__ = _make_arithmetic_op(ops.rmul, cls)
50365034
cls.__rpow__ = _make_arithmetic_op(ops.rpow, cls)
50375035
cls.__pow__ = _make_arithmetic_op(operator.pow, cls)
5036+
5037+
cls.__truediv__ = _make_arithmetic_op(operator.truediv, cls)
5038+
cls.__rtruediv__ = _make_arithmetic_op(ops.rtruediv, cls)
5039+
if not compat.PY3:
5040+
cls.__div__ = _make_arithmetic_op(operator.div, cls)
5041+
cls.__rdiv__ = _make_arithmetic_op(ops.rdiv, cls)
5042+
5043+
# TODO: rmod? rdivmod?
50385044
cls.__mod__ = _make_arithmetic_op(operator.mod, cls)
50395045
cls.__floordiv__ = _make_arithmetic_op(operator.floordiv, cls)
50405046
cls.__rfloordiv__ = _make_arithmetic_op(ops.rfloordiv, cls)
5041-
5042-
if not issubclass(cls, ABCTimedeltaIndex):
5043-
# GH#23829 TimedeltaIndex defines these directly
5044-
cls.__truediv__ = _make_arithmetic_op(operator.truediv, cls)
5045-
cls.__rtruediv__ = _make_arithmetic_op(ops.rtruediv, cls)
5046-
if not compat.PY3:
5047-
cls.__div__ = _make_arithmetic_op(operator.div, cls)
5048-
cls.__rdiv__ = _make_arithmetic_op(ops.rdiv, cls)
5049-
50505047
cls.__divmod__ = _make_arithmetic_op(divmod, cls)
5048+
cls.__mul__ = _make_arithmetic_op(operator.mul, cls)
5049+
cls.__rmul__ = _make_arithmetic_op(ops.rmul, cls)
50515050

50525051
@classmethod
50535052
def _add_numeric_methods_unary(cls):

pandas/core/indexes/datetimelike.py

+6
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,12 @@ def wrap_arithmetic_op(self, other, result):
573573
if result is NotImplemented:
574574
return NotImplemented
575575

576+
if isinstance(result, tuple):
577+
# divmod, rdivmod
578+
assert len(result) == 2
579+
return (wrap_arithmetic_op(self, other, result[0]),
580+
wrap_arithmetic_op(self, other, result[1]))
581+
576582
if not isinstance(result, Index):
577583
# Index.__new__ will choose appropriate subclass for dtype
578584
result = Index(result)

pandas/core/indexes/timedeltas.py

+27-10
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,24 @@
3131
from pandas.tseries.frequencies import to_offset
3232

3333

34-
class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin, Int64Index):
34+
def _make_wrapped_arith_op(opname):
35+
36+
meth = getattr(TimedeltaArray, opname)
37+
38+
def method(self, other):
39+
oth = other
40+
if isinstance(other, Index):
41+
oth = other._data
42+
43+
result = meth(self, oth)
44+
return wrap_arithmetic_op(self, other, result)
45+
46+
method.__name__ = opname
47+
return method
48+
49+
50+
class TimedeltaIndex(TimedeltaArray, DatetimeIndexOpsMixin,
51+
dtl.TimelikeOps, Int64Index):
3552
"""
3653
Immutable ndarray of timedelta64 data, represented internally as int64, and
3754
which can be boxed to timedelta objects
@@ -203,10 +220,6 @@ def _maybe_update_attributes(self, attrs):
203220
attrs['freq'] = 'infer'
204221
return attrs
205222

206-
def _evaluate_with_timedelta_like(self, other, op):
207-
result = TimedeltaArray._evaluate_with_timedelta_like(self, other, op)
208-
return wrap_arithmetic_op(self, other, result)
209-
210223
# -------------------------------------------------------------------
211224
# Rendering Methods
212225

@@ -224,10 +237,14 @@ def _format_native_types(self, na_rep=u'NaT', date_format=None, **kwargs):
224237
# -------------------------------------------------------------------
225238
# Wrapping TimedeltaArray
226239

227-
__mul__ = Index.__mul__
228-
__rmul__ = Index.__rmul__
229-
__floordiv__ = Index.__floordiv__
230-
__rfloordiv__ = Index.__rfloordiv__
240+
__mul__ = _make_wrapped_arith_op("__mul__")
241+
__rmul__ = _make_wrapped_arith_op("__rmul__")
242+
__floordiv__ = _make_wrapped_arith_op("__floordiv__")
243+
__rfloordiv__ = _make_wrapped_arith_op("__rfloordiv__")
244+
__mod__ = _make_wrapped_arith_op("__mod__")
245+
__rmod__ = _make_wrapped_arith_op("__rmod__")
246+
__divmod__ = _make_wrapped_arith_op("__divmod__")
247+
__rdivmod__ = _make_wrapped_arith_op("__rdivmod__")
231248

232249
days = wrap_field_accessor(TimedeltaArray.days)
233250
seconds = wrap_field_accessor(TimedeltaArray.seconds)
@@ -658,7 +675,7 @@ def delete(self, loc):
658675

659676

660677
TimedeltaIndex._add_comparison_ops()
661-
TimedeltaIndex._add_numeric_methods()
678+
TimedeltaIndex._add_numeric_methods_unary()
662679
TimedeltaIndex._add_logical_methods_disabled()
663680
TimedeltaIndex._add_datetimelike_methods()
664681

0 commit comments

Comments
 (0)