Skip to content
40 changes: 38 additions & 2 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ cpdef int64_t delta_to_nanoseconds(delta) except? -1:
if hasattr(delta, 'delta'):
delta = delta.delta
if is_timedelta64_object(delta):
_require_td64_has_unit(delta)
return delta.astype("timedelta64[ns]").item()
if is_integer_object(delta):
return delta
Expand Down Expand Up @@ -447,11 +448,14 @@ cdef inline timedelta_from_spec(object number, object frac, object unit):
# ----------------------------------------------------------------------
# Timedelta ops utilities

cdef bint _validate_ops_compat(other):
cdef bint _validate_ops_compat(other) except -1:
# return True if we are compat with operating
if checknull_with_nat(other):
return True
elif PyDelta_Check(other) or is_timedelta64_object(other):
elif is_timedelta64_object(other):
_require_td64_has_unit(other)
return True
elif PyDelta_Check(other):
return True
elif is_string_object(other):
return True
Expand Down Expand Up @@ -497,6 +501,7 @@ def _binary_op_method_timedeltalike(op, name):
if other.dtype.kind not in ['m', 'M']:
# raise rathering than letting numpy return wrong answer
return NotImplemented
_require_td64_has_unit(other)
return op(self.to_timedelta64(), other)

elif not _validate_ops_compat(other):
Expand Down Expand Up @@ -956,6 +961,8 @@ class Timedelta(_Timedelta):
elif is_timedelta64_object(value):
if unit is not None:
value = value.astype('timedelta64[{0}]'.format(unit))
else:
_require_td64_has_unit(value)
value = value.astype('timedelta64[ns]')
elif hasattr(value, 'delta'):
value = np.timedelta64(delta_to_nanoseconds(value.delta), 'ns')
Expand Down Expand Up @@ -1054,6 +1061,7 @@ class Timedelta(_Timedelta):
def __mul__(self, other):
if hasattr(other, 'dtype'):
# ndarray-like
_require_td64_has_unit(other)
return other * self.to_timedelta64()

elif other is NaT:
Expand All @@ -1069,6 +1077,7 @@ class Timedelta(_Timedelta):

def __truediv__(self, other):
if hasattr(other, 'dtype'):
_require_td64_has_unit(other)
return self.to_timedelta64() / other

elif is_integer_object(other) or is_float_object(other):
Expand All @@ -1085,6 +1094,7 @@ class Timedelta(_Timedelta):

def __rtruediv__(self, other):
if hasattr(other, 'dtype'):
_require_td64_has_unit(other)
return other / self.to_timedelta64()

elif not _validate_ops_compat(other):
Expand Down Expand Up @@ -1116,6 +1126,7 @@ class Timedelta(_Timedelta):
elif hasattr(other, 'dtype'):
if other.dtype.kind == 'm':
# also timedelta-like
_require_td64_has_unit(other)
return _broadcast_floordiv_td64(self.value, other, _floordiv)
elif other.dtype.kind in ['i', 'u', 'f']:
if other.ndim == 0:
Expand Down Expand Up @@ -1155,6 +1166,7 @@ class Timedelta(_Timedelta):
elif hasattr(other, 'dtype'):
if other.dtype.kind == 'm':
# also timedelta-like
_require_td64_has_unit(other)
return _broadcast_floordiv_td64(self.value, other, _rfloordiv)
raise TypeError('Invalid dtype {dtype} for '
'{op}'.format(dtype=other.dtype,
Expand Down Expand Up @@ -1237,6 +1249,30 @@ cdef _broadcast_floordiv_td64(int64_t value, object other,
return res


cdef _require_td64_has_unit(value):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type value (object) you can also add inline

"""
Require that a timedelta64 scalar have a unit specified,
i.e. 'timedelta64[ns]' is OK but 'timedelta64' is not.

Parameters
----------
value : np.timedelta64 or ndarray[timedelta64]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this always true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite; good catch.


Raises
------
ValueError
"""
# GH#19388
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the issue number

if value.dtype == 'm8':
# i.e. has no unit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pls make this more concise (e.g. a single if)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exception message could be shortened, but everything else is needed. Combining the two ifs into one line would hurt clarity.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can just use missing.is_null_datetimelike instead of repeating code here. pls use a single if.

if util.is_array(value) or (value.view('i8') != NPY_NAT):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just do a direct check here

In [1]: np.timedelta64('NaT') Out[1]: numpy.timedelta64('NaT') In [2]: np.timedelta64('NaT') is np.timedelta64(1, 'ns') Out[2]: False In [3]: np.timedelta64('NaT') is np.array([1], dtype='m8[ns]') Out[3]: False 

IOW only if it IS np.timedelta64('NaT') will it be ok

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't work like pd.NaT:

>>> np.timedelta64('NaT') is np.timedelta64('NaT') False 
# If it is timedelta64('NaT') then its meaning is unambigous
# even though it does not have a unit.
raise TypeError('Cannot operate on timedelta64 value without a '
'unit specified. Try casting to "m8[ns]" to '
'explicitly specify nanosecond unit.')


# resolution in ns
Timedelta.min = Timedelta(np.iinfo(np.int64).min + 1)
Timedelta.max = Timedelta(np.iinfo(np.int64).max)
4 changes: 2 additions & 2 deletions pandas/tests/indexes/timedeltas/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -1055,8 +1055,8 @@ def test_timedelta(self, freq):
rng = date_range('2013', '2014')
s = Series(rng)
result1 = rng - pd.offsets.Hour(1)
result2 = DatetimeIndex(s - np.timedelta64(100000000))
result3 = rng - np.timedelta64(100000000)
result2 = DatetimeIndex(s - np.timedelta64(100000000, 'ns'))
result3 = rng - np.timedelta64(100000000, 'ns')
result4 = DatetimeIndex(s - pd.offsets.Hour(1))
tm.assert_index_equal(result1, result4)
tm.assert_index_equal(result2, result3)
29 changes: 29 additions & 0 deletions pandas/tests/scalar/timedelta/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -614,3 +614,32 @@ def test_rdivmod_invalid(self):

with pytest.raises(TypeError):
divmod(np.array([22, 24]), td)


def test_require_td64_unit():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in test_constructor

# GH#19388
td64 = np.timedelta64(86401)
nat64 = np.timedelta64('NaT')

with pytest.raises(TypeError):
# Timedelta constructor should not allow unit-less timedelta64
Timedelta(td64)

assert Timedelta(nat64) is NaT
assert Timedelta(td64, unit='s').total_seconds() == 86401


@pytest.mark.parametrize('op', [operator.add, ops.radd,
operator.sub, ops.rsub,
operator.mul, ops.rmul,
operator.floordiv, ops.rfloordiv,
operator.mod, ops.rmod,
divmod, ops.rdivmod])
@pytest.mark.parametrize('other', [
np.timedelta64(86401),
np.array([86401], dtype='timedelta64')])
def test_require_td64_unit_ops(op, other):
# GH#19388
td = Timedelta(days=3, hours=4, minutes=5)
with pytest.raises(TypeError):
op(td, other)
2 changes: 1 addition & 1 deletion pandas/tests/series/test_missing.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def test_timedelta_fillna(self):
timedelta(days=1, seconds=9 * 3600 + 60 + 1)])
assert_series_equal(result, expected)

result = td.fillna(np.timedelta64(int(1e9)))
result = td.fillna(np.timedelta64(int(1e9), 'ns'))
expected = Series([timedelta(seconds=1), timedelta(0), timedelta(1),
timedelta(days=1, seconds=9 * 3600 + 60 + 1)])
assert_series_equal(result, expected)
Expand Down