Skip to content

Commit 4d2d571

Browse files
committed
BUG: (GH4134) Fix arithmetic with series/datetimeindex and np.timedelta64 not working the same
TST: add in @cpcloud tests related to GH4135
1 parent 6f8550a commit 4d2d571

File tree

7 files changed

+393
-241
lines changed

7 files changed

+393
-241
lines changed

doc/source/release.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ pandas 0.13
168168
- Fixed (:issue:`3334`) in pivot_table. Margins did not compute if values is the index.
169169
- Fix bug in having a rhs of ``np.timedelta64`` or ``np.offsets.DateOffset`` when operating
170170
with datetimes (:issue:`4532`)
171+
- Fix arithmetic with series/datetimeindex and ``np.timedelta64`` not working the same (:issue:`4134`)
172+
and buggy timedelta in numpy 1.6 (:issue:`4135`)
171173

172174
pandas 0.12
173175
===========

pandas/core/common.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
import pandas.algos as algos
1212
import pandas.lib as lib
1313
import pandas.tslib as tslib
14-
14+
from distutils.version import LooseVersion
1515
from pandas import compat
1616
from pandas.compat import StringIO, BytesIO, range, long, u, zip, map
17+
from datetime import timedelta
18+
1719
from pandas.core.config import get_option
1820
from pandas.core import array as pa
1921

@@ -33,6 +35,10 @@ class PandasError(Exception):
3335
class AmbiguousIndexError(PandasError, KeyError):
3436
pass
3537

38+
# versioning
39+
_np_version = np.version.short_version
40+
_np_version_under1p6 = LooseVersion(_np_version) < '1.6'
41+
_np_version_under1p7 = LooseVersion(_np_version) < '1.7'
3642

3743
_POSSIBLY_CAST_DTYPES = set([ np.dtype(t) for t in ['M8[ns]','m8[ns]','O','int8','uint8','int16','uint16','int32','uint32','int64','uint64'] ])
3844
_NS_DTYPE = np.dtype('M8[ns]')
@@ -1144,7 +1150,45 @@ def _possibly_convert_platform(values):
11441150
def _possibly_cast_to_timedelta(value, coerce=True):
11451151
""" try to cast to timedelta64, if already a timedeltalike, then make
11461152
sure that we are [ns] (as numpy 1.6.2 is very buggy in this regards,
1147-
don't force the conversion unless coerce is True """
1153+
don't force the conversion unless coerce is True
1154+
1155+
if coerce='compat' force a compatibilty coercerion (to timedeltas) if needeed
1156+
"""
1157+
1158+
# coercion compatability
1159+
if coerce == 'compat' and _np_version_under1p7:
1160+
1161+
def convert(td, type):
1162+
1163+
# we have an array with a non-object dtype
1164+
if hasattr(td,'item'):
1165+
td = td.astype(np.int64).item()
1166+
if td == tslib.iNaT:
1167+
return td
1168+
if dtype == 'm8[us]':
1169+
td *= 1000
1170+
return td
1171+
1172+
if td == tslib.compat_NaT:
1173+
return tslib.iNaT
1174+
1175+
# convert td value to a nanosecond value
1176+
d = td.days
1177+
s = td.seconds
1178+
us = td.microseconds
1179+
1180+
if dtype == 'object' or dtype == 'm8[ns]':
1181+
td = 1000*us + (s + d * 24 * 3600) * 10 ** 9
1182+
else:
1183+
raise ValueError("invalid conversion of dtype in np < 1.7 [%s]" % dtype)
1184+
1185+
return td
1186+
1187+
# < 1.7 coercion
1188+
if not is_list_like(value):
1189+
value = np.array([ value ])
1190+
dtype = value.dtype
1191+
return np.array([ convert(v,dtype) for v in value ], dtype='m8[ns]')
11481192

11491193
# deal with numpy not being able to handle certain timedelta operations
11501194
if isinstance(value,np.ndarray) and value.dtype.kind == 'm':
@@ -1154,6 +1198,7 @@ def _possibly_cast_to_timedelta(value, coerce=True):
11541198

11551199
# we don't have a timedelta, but we want to try to convert to one (but don't force it)
11561200
if coerce:
1201+
11571202
new_value = tslib.array_to_timedelta64(value.astype(object), coerce=False)
11581203
if new_value.dtype == 'i8':
11591204
value = np.array(new_value,dtype='timedelta64[ns]')

pandas/core/series.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ def wrapper(self, other, name=name):
9494

9595
if is_datetime_lhs or is_timedelta_lhs:
9696

97+
coerce = 'compat' if _np_version_under1p7 else True
98+
9799
# convert the argument to an ndarray
98100
def convert_to_array(values):
99101
if not is_list_like(values):
@@ -105,19 +107,20 @@ def convert_to_array(values):
105107
values = tslib.array_to_datetime(values)
106108
elif inferred_type in set(['timedelta']):
107109
# have a timedelta, convert to to ns here
108-
if not (isinstance(values, pa.Array) and com.is_timedelta64_dtype(values)):
109-
values = com._possibly_cast_to_timedelta(values)
110+
values = com._possibly_cast_to_timedelta(values, coerce=coerce)
110111
elif inferred_type in set(['timedelta64']):
111112
# have a timedelta64, make sure dtype dtype is ns
112-
values = com._possibly_cast_to_timedelta(values)
113+
values = com._possibly_cast_to_timedelta(values, coerce=coerce)
113114
elif inferred_type in set(['integer']):
114115
# py3 compat where dtype is 'm' but is an integer
115116
if values.dtype.kind == 'm':
116117
values = values.astype('timedelta64[ns]')
118+
else:
119+
raise ValueError("incompatible type for a datetime/timedelta operation")
117120
elif isinstance(values[0],DateOffset):
118121
# handle DateOffsets
119122
values = pa.array([ v.delta for v in values ])
120-
values = com._possibly_cast_to_timedelta(values)
123+
values = com._possibly_cast_to_timedelta(values, coerce=coerce)
121124
else:
122125
values = pa.array(values)
123126
return values
@@ -126,8 +129,8 @@ def convert_to_array(values):
126129
lvalues = convert_to_array(lvalues)
127130
rvalues = convert_to_array(rvalues)
128131

129-
is_timedelta_rhs = com.is_timedelta64_dtype(rvalues)
130132
is_datetime_rhs = com.is_datetime64_dtype(rvalues)
133+
is_timedelta_rhs = com.is_timedelta64_dtype(rvalues) or (not is_datetime_rhs and _np_version_under1p7)
131134

132135
# 2 datetimes or 2 timedeltas
133136
if (is_timedelta_lhs and is_timedelta_rhs) or (is_datetime_lhs and
@@ -141,7 +144,6 @@ def convert_to_array(values):
141144

142145
dtype = 'timedelta64[ns]'
143146

144-
# we may have to convert to object unfortunately here
145147
mask = isnull(lvalues) | isnull(rvalues)
146148
if mask.any():
147149
def wrap_results(x):
@@ -150,13 +152,20 @@ def wrap_results(x):
150152
return x
151153

152154
# datetime and timedelta
153-
elif (is_timedelta_lhs and is_datetime_rhs) or (is_timedelta_rhs and is_datetime_lhs):
155+
elif is_timedelta_rhs and is_datetime_lhs:
154156

155157
if name not in ['__add__','__sub__']:
156-
raise TypeError("can only operate on a timedelta and a datetime for "
158+
raise TypeError("can only operate on a datetime with a rhs of a timedelta for "
157159
"addition and subtraction, but the operator [%s] was passed" % name)
158160
dtype = 'M8[ns]'
159161

162+
elif is_timedelta_lhs and is_datetime_rhs:
163+
164+
if name not in ['__add__']:
165+
raise TypeError("can only operate on a timedelta and a datetime for "
166+
"addition, but the operator [%s] was passed" % name)
167+
dtype = 'M8[ns]'
168+
160169
else:
161170
raise ValueError('cannot operate on a series with out a rhs '
162171
'of a series/ndarray of type datetime64[ns] '
@@ -166,8 +175,11 @@ def wrap_results(x):
166175
rvalues = rvalues.view('i8')
167176

168177
if isinstance(rvalues, Series):
169-
lvalues = lvalues.values
170-
rvalues = rvalues.values
178+
179+
if hasattr(lvalues,'values'):
180+
lvalues = lvalues.values
181+
if hasattr(rvalues,'values'):
182+
rvalues = rvalues.values
171183

172184
if self.index.equals(other.index):
173185
name = _maybe_match_name(self, other)

0 commit comments

Comments
 (0)