-
- Notifications
You must be signed in to change notification settings - Fork 19.4k
disallow Timedelta ops with unitless timedelta64 #19870
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
f098732 691ebd6 4ea3752 06384e6 de47608 658c23b affba36 5da9548 966f150 15f6f5f 507cb32 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -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 | ||
| | @@ -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 | ||
| | @@ -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): | ||
| | @@ -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') | ||
| | @@ -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: | ||
| | @@ -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): | ||
| | @@ -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): | ||
| | @@ -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: | ||
| | @@ -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, | ||
| | @@ -1237,6 +1249,30 @@ cdef _broadcast_floordiv_td64(int64_t value, object other, | |
| return res | ||
| | ||
| | ||
| cdef _require_td64_has_unit(value): | ||
| """ | ||
| 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] | ||
| ||
| | ||
| Raises | ||
| ------ | ||
| ValueError | ||
| """ | ||
| # GH#19388 | ||
| ||
| if value.dtype == 'm8': | ||
| # i.e. has no unit | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pls make this more concise (e.g. a single if) Member Author There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can just use | ||
| if util.is_array(value) or (value.view('i8') != NPY_NAT): | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can just do a direct check here IOW only if it IS Member Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't work like | ||
| # 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) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -614,3 +614,32 @@ def test_rdivmod_invalid(self): | |
| | ||
| with pytest.raises(TypeError): | ||
| divmod(np.array([22, 24]), td) | ||
| | ||
| | ||
| def test_require_td64_unit(): | ||
| ||
| # 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) | ||
There was a problem hiding this comment.
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