From 4688064fafec76a6c68bc0238d3df83e64185671 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 23 Jan 2018 19:26:53 -0800 Subject: [PATCH 01/15] implement mod, divmod, rmod, rdivmod, fix and test scalar methods --- pandas/_libs/tslibs/timedeltas.pyx | 47 ++++++- pandas/tests/scalar/test_timedelta.py | 177 +++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 5 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index b2c9c464c7cbf..c0512a782cbea 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -492,7 +492,14 @@ 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 - return op(self.to_timedelta64(), other) + result = op(self.to_timedelta64(), other) + if other.ndim == 0: + if other.dtype.kind == 'm': + return Timedelta(result) + if other.dtype.kind == 'M': + from ..tslib import Timestamp + return Timestamp(result) + return result elif not _validate_ops_compat(other): return NotImplemented @@ -1046,7 +1053,10 @@ class Timedelta(_Timedelta): def __mul__(self, other): if hasattr(other, 'dtype'): # ndarray-like - return other * self.to_timedelta64() + result = other * self.to_timedelta64() + if other.ndim == 0: + return Timedelta(result) + return result elif other is NaT: return NaT @@ -1061,7 +1071,10 @@ class Timedelta(_Timedelta): def __truediv__(self, other): if hasattr(other, 'dtype'): - return self.to_timedelta64() / other + result = self.to_timedelta64() / other + if other.ndim == 0 and result.dtype.kind == 'm': + return Timedelta(result) + return result elif is_integer_object(other) or is_float_object(other): # integers or floats @@ -1077,7 +1090,10 @@ class Timedelta(_Timedelta): def __rtruediv__(self, other): if hasattr(other, 'dtype'): - return other / self.to_timedelta64() + result = other / self.to_timedelta64() + if other.ndim == 0 and result.dtype.kind == 'm': + return Timedelta(result) + return result elif not _validate_ops_compat(other): return NotImplemented @@ -1096,6 +1112,9 @@ class Timedelta(_Timedelta): # just defer if hasattr(other, '_typ'): # Series, DataFrame, ... + if other._typ == 'dateoffset' and hasattr(other, 'delta'): + # Tick offset + return self // other.delta return NotImplemented if hasattr(other, 'dtype'): @@ -1128,6 +1147,9 @@ class Timedelta(_Timedelta): # just defer if hasattr(other, '_typ'): # Series, DataFrame, ... + if other._typ == 'dateoffset' and hasattr(other, 'delta'): + # Tick offset + return other.delta // self return NotImplemented if hasattr(other, 'dtype'): @@ -1149,6 +1171,23 @@ class Timedelta(_Timedelta): return np.nan return other.value // self.value + def __mod__(self, other): + # Naive implementation, room for optimization + return self.__divmod__(other)[1] + + def __rmod__(self, other): + # Naive implementation, room for optimization + return self.__rdivmod__(other)[1] + + def __divmod__(self, other): + # Naive implementation, room for optimization + div = self // other + return div, self - div * other + + def __rdivmod__(self, other): + div = other // self + return div, other - div * self + cdef _floordiv(int64_t value, right): return value // right diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 64d4940082978..7eda8b78fffae 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -2,7 +2,7 @@ import pytest import numpy as np -from datetime import timedelta +from datetime import datetime, timedelta import pandas as pd import pandas.util.testing as tm @@ -128,6 +128,57 @@ def test_unary_ops(self): assert abs(-td) == td assert abs(-td) == Timedelta('10d') + def test_mul(self): + td = Timedelta(minutes=3) + + result = td * 2 + assert result == Timedelta(minutes=6) + + result = td * np.int64(1) + assert isinstance(result, Timedelta) + assert result == td + + result = td * 1.5 + assert result == Timedelta(minutes=4, seconds=30) + + result = td * np.array([3, 4], dtype='int64') + expected = np.array([9, 12], dtype='m8[m]').astype('m8[ns]') + tm.assert_numpy_array_equal(result, expected) + + with pytest.raises(TypeError): + td * pd.Timestamp(2016, 1, 2) + + def test_add_datetimelike(self): + td = Timedelta(10, unit='d') + + result = td + datetime(2016, 1, 1) + assert result == pd.Timestamp(2016, 1, 11) + + result = td + pd.Timestamp('2018-01-12 18:09') + assert result == pd.Timestamp('2018-01-22 18:09') + + result = td + np.datetime64('2018-01-12') + assert result == pd.Timestamp('2018-01-22') + + def test_add_timedeltalike(self): + td = Timedelta(10, unit='d') + + result = td + Timedelta(days=10) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=20) + + result = td + timedelta(days=9) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=19) + + result = td + pd.offsets.Hour(6) + assert isinstance(result, Timedelta) + assert result == Timedelta(days=10, hours=6) + + result = td + np.timedelta64(-4, 'D') + assert isinstance(result, Timedelta) + assert result == Timedelta(days=6) + def test_binary_ops_nat(self): td = Timedelta(10, unit='d') @@ -138,6 +189,9 @@ def test_binary_ops_nat(self): assert (td // pd.NaT) is np.nan assert (td // np.timedelta64('NaT')) is np.nan + assert td - np.timedelta64('NaT') is pd.NaT + assert td + np.timedelta64('NaT') is pd.NaT + def test_binary_ops_integers(self): td = Timedelta(10, unit='d') @@ -173,6 +227,9 @@ def test_floordiv(self): assert -td // scalar.to_pytimedelta() == -2 assert (2 * td) // scalar.to_timedelta64() == 2 + assert td // pd.offsets.Hour(1) == 3 + assert td // pd.offsets.Minute(2) == 92 + assert td // np.nan is pd.NaT assert np.isnan(td // pd.NaT) assert np.isnan(td // np.timedelta64('NaT')) @@ -218,6 +275,8 @@ def test_rfloordiv(self): assert (-td).__rfloordiv__(scalar.to_pytimedelta()) == -2 assert (2 * td).__rfloordiv__(scalar.to_timedelta64()) == 0 + assert pd.offsets.Hour(1) // Timedelta(minutes=25) == 2 + assert np.isnan(td.__rfloordiv__(pd.NaT)) assert np.isnan(td.__rfloordiv__(np.timedelta64('NaT'))) @@ -255,6 +314,122 @@ def test_rfloordiv(self): with pytest.raises(TypeError): ser // td + def test_mod(self): + td = Timedelta(hours=37) + + # Timedelta-like others + result = td % Timedelta(hours=6) + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=1) + + result = td % timedelta(minutes=60) + assert isinstance(result, Timedelta) + assert result == Timedelta(0) + + result = td % pd.offsets.Hour(5) + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=2) + + result = td % np.timedelta64(2, 'h') + assert isinstance(result, Timedelta) + assert result == Timedelta(hours=1) + + result = td % NaT + assert result is NaT + + result = td % np.timedelta64('NaT') + assert result is NaT + + # Numeric Others + result = td % 2 + assert isinstance(result, Timedelta) + assert result == Timedelta(0) + + result = td % 1e12 + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=3, seconds=20) + + result = td % int(1e12) + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=3, seconds=20) + + # Invalid Others + with pytest.raises(TypeError): + td % pd.Timestamp('2018-01-22') + + # Array-like others + result = td % np.array([6, 5], dtype='timedelta64[h]') + expected = np.array([1, 2], dtype='timedelta64[h]').astype('m8[ns]') + tm.assert_numpy_array_equal(result, expected) + + result = td % pd.TimedeltaIndex(['6H', '5H']) + expected = pd.TimedeltaIndex(['1H', '2H']) + tm.assert_index_equal(result, expected) + + result = td % np.array([2, int(1e12)], dtype='i8') + expected = np.array([0, Timedelta(minutes=3, seconds=20).value], + dtype='m8[ns]') + tm.assert_numpy_array_equal(result, expected) + + def test_rmod(self): + td = Timedelta(minutes=3) + + result = timedelta(minutes=4) % td + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=1) + + result = np.timedelta64(5, 'm') % td + assert isinstance(result, Timedelta) + assert result == Timedelta(minutes=2) + + result = np.array([5, 6], dtype='m8[m]') % td + expected = np.array([2, 0], dtype='m8[m]').astype('m8[ns]') + tm.assert_numpy_array_equal(result, expected) + + def test_divmod(self): + td = Timedelta(days=2, hours=6) + + result = divmod(td, timedelta(days=1)) + assert result[0] == 2 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=6) + + result = divmod(td, pd.offsets.Hour(-4)) + assert result[0] == -14 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=-2) + + result = divmod(td, 54) + assert result[0] == Timedelta(hours=1) + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(0) + + result = divmod(td, 53 * 3600 * 1e9) + assert result[0] == Timedelta(1, unit='ns') + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=1) + + assert result + result = divmod(td, np.nan) + assert result[0] is pd.NaT + assert result[1] is pd.NaT + + result = divmod(td, pd.NaT) + assert np.isnan(result[0]) + assert result[1] is pd.NaT + + def test_rdivmod(self): + + result = divmod(timedelta(days=2, hours=6), Timedelta(days=1)) + assert result[0] == 2 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=6) + + result = divmod(pd.offsets.Hour(54), Timedelta(hours=-4)) + assert result[0] == -14 + assert isinstance(result[1], Timedelta) + assert result[1] == Timedelta(hours=-2) + class TestTimedeltaComparison(object): def test_comparison_object_array(self): From baf4d6bcf1a5c4d917590942b128e49dc11f4a3e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 24 Jan 2018 10:12:40 -0800 Subject: [PATCH 02/15] whatsnew, gh reference in tests --- doc/source/whatsnew/v0.23.0.txt | 1 + pandas/tests/scalar/test_timedelta.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 71492154419fb..0569bb4eefcca 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -419,6 +419,7 @@ Datetimelike - Bug in ``.astype()`` to non-ns timedelta units would hold the incorrect dtype (:issue:`19176`, :issue:`19223`, :issue:`12425`) - Bug in subtracting :class:`Series` from ``NaT`` incorrectly returning ``NaT`` (:issue:`19158`) - Bug in :func:`Series.truncate` which raises ``TypeError`` with a monotonic ``PeriodIndex`` (:issue:`17717`) +- Bug :func:`Timedelta.__mod__`, :func:`Timedelta.__divmod__` where operating with timedelta-like or numeric arguments would incorrectly raise ``TypeError`` (:issue:`19365`) Timezones ^^^^^^^^^ diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 7eda8b78fffae..ebcbcb2b496d4 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -129,6 +129,7 @@ def test_unary_ops(self): assert abs(-td) == Timedelta('10d') def test_mul(self): + # GH#19365 td = Timedelta(minutes=3) result = td * 2 @@ -149,6 +150,7 @@ def test_mul(self): td * pd.Timestamp(2016, 1, 2) def test_add_datetimelike(self): + # GH#19365 td = Timedelta(10, unit='d') result = td + datetime(2016, 1, 1) @@ -189,6 +191,7 @@ def test_binary_ops_nat(self): assert (td // pd.NaT) is np.nan assert (td // np.timedelta64('NaT')) is np.nan + # GH#19365 assert td - np.timedelta64('NaT') is pd.NaT assert td + np.timedelta64('NaT') is pd.NaT @@ -227,6 +230,7 @@ def test_floordiv(self): assert -td // scalar.to_pytimedelta() == -2 assert (2 * td) // scalar.to_timedelta64() == 2 + # GH#19365 assert td // pd.offsets.Hour(1) == 3 assert td // pd.offsets.Minute(2) == 92 @@ -315,6 +319,7 @@ def test_rfloordiv(self): ser // td def test_mod(self): + # GH#19365 td = Timedelta(hours=37) # Timedelta-like others @@ -372,6 +377,7 @@ def test_mod(self): tm.assert_numpy_array_equal(result, expected) def test_rmod(self): + # GH#19365 td = Timedelta(minutes=3) result = timedelta(minutes=4) % td @@ -387,6 +393,7 @@ def test_rmod(self): tm.assert_numpy_array_equal(result, expected) def test_divmod(self): + # GH#19365 td = Timedelta(days=2, hours=6) result = divmod(td, timedelta(days=1)) @@ -419,7 +426,7 @@ def test_divmod(self): assert result[1] is pd.NaT def test_rdivmod(self): - + # GH#19365 result = divmod(timedelta(days=2, hours=6), Timedelta(days=1)) assert result[0] == 2 assert isinstance(result[1], Timedelta) From a2b1ac7ab793920ad46f1720d164d7a7f4a1abaf Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 24 Jan 2018 18:36:43 -0800 Subject: [PATCH 03/15] separate tests for invalid mod and divmod --- pandas/tests/scalar/test_timedelta.py | 75 +++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index ebcbcb2b496d4..c75b2f92e2b01 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -162,22 +162,24 @@ def test_add_datetimelike(self): result = td + np.datetime64('2018-01-12') assert result == pd.Timestamp('2018-01-22') - def test_add_timedeltalike(self): + @pytest.mark.parametrize('op', [lambda x, y: x + y, + lambda x, y: y + x]) + def test_add_timedeltalike(self, op): td = Timedelta(10, unit='d') - result = td + Timedelta(days=10) + result = op(td, Timedelta(days=10)) assert isinstance(result, Timedelta) assert result == Timedelta(days=20) - result = td + timedelta(days=9) + result = op(td, timedelta(days=9)) assert isinstance(result, Timedelta) assert result == Timedelta(days=19) - result = td + pd.offsets.Hour(6) + result = op(td, pd.offsets.Hour(6)) assert isinstance(result, Timedelta) assert result == Timedelta(days=10, hours=6) - result = td + np.timedelta64(-4, 'D') + result = op(td, np.timedelta64(-4, 'D')) assert isinstance(result, Timedelta) assert result == Timedelta(days=6) @@ -194,6 +196,8 @@ def test_binary_ops_nat(self): # GH#19365 assert td - np.timedelta64('NaT') is pd.NaT assert td + np.timedelta64('NaT') is pd.NaT + assert np.timedelta64('NaT') - td is pd.NaT + assert np.timedelta64('NaT') + td is pd.NaT def test_binary_ops_integers(self): td = Timedelta(10, unit='d') @@ -318,7 +322,7 @@ def test_rfloordiv(self): with pytest.raises(TypeError): ser // td - def test_mod(self): + def test_mod_timedeltalike(self): # GH#19365 td = Timedelta(hours=37) @@ -345,6 +349,10 @@ def test_mod(self): result = td % np.timedelta64('NaT') assert result is NaT + def test_mod_numeric(self): + # GH#19365 + td = Timedelta(hours=37) + # Numeric Others result = td % 2 assert isinstance(result, Timedelta) @@ -358,9 +366,9 @@ def test_mod(self): assert isinstance(result, Timedelta) assert result == Timedelta(minutes=3, seconds=20) - # Invalid Others - with pytest.raises(TypeError): - td % pd.Timestamp('2018-01-22') + def test_mod_arraylike(self): + # GH#19365 + td = Timedelta(hours=37) # Array-like others result = td % np.array([6, 5], dtype='timedelta64[h]') @@ -376,6 +384,16 @@ def test_mod(self): dtype='m8[ns]') tm.assert_numpy_array_equal(result, expected) + def test_mod_invalid(self): + # GH#19365 + td = Timedelta(hours=37) + + with pytest.raises(TypeError): + td % pd.Timestamp('2018-01-22') + + with pytest.raises(TypeError): + td % [] + def test_rmod(self): # GH#19365 td = Timedelta(minutes=3) @@ -392,6 +410,22 @@ def test_rmod(self): expected = np.array([2, 0], dtype='m8[m]').astype('m8[ns]') tm.assert_numpy_array_equal(result, expected) + def test_rmod_invalid(self): + # GH#19365 + td = Timedelta(minutes=3) + + with pytest.raises(TypeError): + pd.Timestamp('2018-01-22') % td + + with pytest.raises(TypeError): + 15 % td + + with pytest.raises(TypeError): + 16.0 % td + + with pytest.raises(TypeError): + np.array([22, 24]) % td + def test_divmod(self): # GH#19365 td = Timedelta(days=2, hours=6) @@ -425,6 +459,13 @@ def test_divmod(self): assert np.isnan(result[0]) assert result[1] is pd.NaT + def test_divmod_invalid(self): + # GH#19365 + td = Timedelta(days=2, hours=6) + + with pytest.raises(TypeError): + divmod(td, pd.Timestamp('2018-01-22')) + def test_rdivmod(self): # GH#19365 result = divmod(timedelta(days=2, hours=6), Timedelta(days=1)) @@ -437,6 +478,22 @@ def test_rdivmod(self): assert isinstance(result[1], Timedelta) assert result[1] == Timedelta(hours=-2) + def test_rdivmod_invalid(self): + # GH#19365 + td = Timedelta(minutes=3) + + with pytest.raises(TypeError): + divmod(pd.Timestamp('2018-01-22'), td) + + with pytest.raises(TypeError): + divmod(15, td) + + with pytest.raises(TypeError): + divmod(16.0, td) + + with pytest.raises(TypeError): + divmod(np.array([22, 24]), td) + class TestTimedeltaComparison(object): def test_comparison_object_array(self): From 822487162a7b7a5a78913618d493a17f26fbac9b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 24 Jan 2018 18:52:12 -0800 Subject: [PATCH 04/15] docs --- doc/source/timedeltas.rst | 9 +++++++ doc/source/whatsnew/v0.23.0.txt | 43 ++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/doc/source/timedeltas.rst b/doc/source/timedeltas.rst index 50cff4c7bbdfb..b4e12b9dd7eb9 100644 --- a/doc/source/timedeltas.rst +++ b/doc/source/timedeltas.rst @@ -283,6 +283,15 @@ Rounded division (floor-division) of a ``timedelta64[ns]`` Series by a scalar td // pd.Timedelta(days=3, hours=4) pd.Timedelta(days=3, hours=4) // td +The mod (%) and divmod operations are defined for ``Timedelta`` when operating with another timedelta-like or with a numeric argument. + +.. ipython:: python + + pd.Timedelta(hours=37) % datetime.timedelta(hours=2) + divmod(datetime.timedelta(hours=2), pd.Timedelta(minutes=11)) + divmod(pd.Timedelta(days=7), np.array([2, 3], dtype='timedelta64[D]')) + pd.Timedelta(hours=25) % 86400000000000 + Attributes ---------- diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 0569bb4eefcca..a06f52f5d7945 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -89,6 +89,46 @@ resetting indexes. See the :ref:`Sorting by Indexes and Values # Sort by 'second' (index) and 'A' (column) df_multi.sort_values(by=['second', 'A']) +.. _whatsnew_0230.enhancements.timedelta_mod + +Timedelta mod method +^^^^^^^^^^^^^^^^^^^^ + +mod (%) and divmod operations are now defined on Timedelta objects when operating with either timedelta-like or with numeric arguments. (:issue:`19365`) + +.. ipython:: python + + td = pd.Timedelta(hours=37) + td % pd.Timedelta(hours=2) + divmod(td, np.array([2, 3], dtype='timedelta64[h]')) + +Previous Behavior: + +.. code-block:: ipython + + In [5]: td % pd.Timedelta(hours=2) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 td % pd.Timedelta(hours=2) + + TypeError: unsupported operand type(s) for %: 'Timedelta' and 'Timedelta' + + In [6]: divmod(td, np.array([2, 3], dtype='timedelta64[h]')) + --------------------------------------------------------------------------- + TypeError Traceback (most recent call last) + in () + ----> 1 divmod(td, np.array([2, 3], dtype='timedelta64[h]')) + + TypeError: unsupported operand type(s) for divmod(): 'Timedelta' and 'datetime.timedelta' + +Current Behavior + +.. ipython:: python + + td % pd.Timedelta(hours=2) + divmod(td, np.array([2, 3], dtype='timedelta64[h]')) + .. _whatsnew_0230.enhancements.ran_inf: ``.rank()`` handles ``inf`` values when ``NaN`` are present @@ -313,6 +353,7 @@ Other API Changes - :func:`DatetimeIndex.shift` and :func:`TimedeltaIndex.shift` will now raise ``NullFrequencyError`` (which subclasses ``ValueError``, which was raised in older versions) when the index object frequency is ``None`` (:issue:`19147`) - Addition and subtraction of ``NaN`` from a :class:`Series` with ``dtype='timedelta64[ns]'`` will raise a ``TypeError` instead of treating the ``NaN`` as ``NaT`` (:issue:`19274`) - Set operations (union, difference...) on :class:`IntervalIndex` with incompatible index types will now raise a ``TypeError`` rather than a ``ValueError`` (:issue:`19329`) +- :func:`Timedelta.__mod__`, :func:`Timedelta.__divmod__` now accept timedelta-like and numeric arguments instead of raising ``TypeError`` (:issue:`19365`) .. _whatsnew_0230.deprecations: @@ -419,7 +460,7 @@ Datetimelike - Bug in ``.astype()`` to non-ns timedelta units would hold the incorrect dtype (:issue:`19176`, :issue:`19223`, :issue:`12425`) - Bug in subtracting :class:`Series` from ``NaT`` incorrectly returning ``NaT`` (:issue:`19158`) - Bug in :func:`Series.truncate` which raises ``TypeError`` with a monotonic ``PeriodIndex`` (:issue:`17717`) -- Bug :func:`Timedelta.__mod__`, :func:`Timedelta.__divmod__` where operating with timedelta-like or numeric arguments would incorrectly raise ``TypeError`` (:issue:`19365`) + Timezones ^^^^^^^^^ From 9558dc9e1507e7fcc83d1de0010c41fb31970c5c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 24 Jan 2018 19:17:34 -0800 Subject: [PATCH 05/15] fix test broken on appveyor --- pandas/tests/scalar/test_timedelta.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index c75b2f92e2b01..028dd6e69536f 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -194,10 +194,10 @@ def test_binary_ops_nat(self): assert (td // np.timedelta64('NaT')) is np.nan # GH#19365 - assert td - np.timedelta64('NaT') is pd.NaT - assert td + np.timedelta64('NaT') is pd.NaT - assert np.timedelta64('NaT') - td is pd.NaT - assert np.timedelta64('NaT') + td is pd.NaT + assert td - np.timedelta64('NaT', 'ns') is pd.NaT + assert td + np.timedelta64('NaT', 'ns') is pd.NaT + assert np.timedelta64('NaT', 'ns') - td is pd.NaT + assert np.timedelta64('NaT', 'ns') + td is pd.NaT def test_binary_ops_integers(self): td = Timedelta(10, unit='d') @@ -346,7 +346,7 @@ def test_mod_timedeltalike(self): result = td % NaT assert result is NaT - result = td % np.timedelta64('NaT') + result = td % np.timedelta64('NaT', 'ns') assert result is NaT def test_mod_numeric(self): From f838cc9262b37cbf9470951f94457178a46166a5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 29 Jan 2018 08:27:35 -0800 Subject: [PATCH 06/15] requested edits --- doc/source/timedeltas.rst | 5 ++++- doc/source/whatsnew/v0.23.0.txt | 23 +---------------------- pandas/tests/scalar/test_timedelta.py | 1 + 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/doc/source/timedeltas.rst b/doc/source/timedeltas.rst index b4e12b9dd7eb9..d1e0d635996de 100644 --- a/doc/source/timedeltas.rst +++ b/doc/source/timedeltas.rst @@ -288,8 +288,11 @@ The mod (%) and divmod operations are defined for ``Timedelta`` when operating w .. ipython:: python pd.Timedelta(hours=37) % datetime.timedelta(hours=2) + + # divmod against a timedelta-like returns a pair (int, Timedelta) divmod(datetime.timedelta(hours=2), pd.Timedelta(minutes=11)) - divmod(pd.Timedelta(days=7), np.array([2, 3], dtype='timedelta64[D]')) + + # divmod against a numeric returns a pair (Timedelta, Timedelta) pd.Timedelta(hours=25) % 86400000000000 Attributes diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index a06f52f5d7945..9f5bce3153ad7 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -99,28 +99,7 @@ mod (%) and divmod operations are now defined on Timedelta objects when operatin .. ipython:: python td = pd.Timedelta(hours=37) - td % pd.Timedelta(hours=2) - divmod(td, np.array([2, 3], dtype='timedelta64[h]')) - -Previous Behavior: - -.. code-block:: ipython - - In [5]: td % pd.Timedelta(hours=2) - --------------------------------------------------------------------------- - TypeError Traceback (most recent call last) - in () - ----> 1 td % pd.Timedelta(hours=2) - - TypeError: unsupported operand type(s) for %: 'Timedelta' and 'Timedelta' - - In [6]: divmod(td, np.array([2, 3], dtype='timedelta64[h]')) - --------------------------------------------------------------------------- - TypeError Traceback (most recent call last) - in () - ----> 1 divmod(td, np.array([2, 3], dtype='timedelta64[h]')) - - TypeError: unsupported operand type(s) for divmod(): 'Timedelta' and 'datetime.timedelta' + td Current Behavior diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 028dd6e69536f..4643c380fae10 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -147,6 +147,7 @@ def test_mul(self): tm.assert_numpy_array_equal(result, expected) with pytest.raises(TypeError): + # timedelta * datetime is gibberish td * pd.Timestamp(2016, 1, 2) def test_add_datetimelike(self): From 1ace83876cbdcafe3daee4d404d7343b197b3fdd Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 9 Feb 2018 08:13:53 -0800 Subject: [PATCH 07/15] de-dup tests, edit whatsnew --- doc/source/whatsnew/v0.23.0.txt | 2 +- pandas/tests/scalar/test_timedelta.py | 30 +++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 167ef33935702..187444e3a1e84 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -122,7 +122,7 @@ resetting indexes. See the :ref:`Sorting by Indexes and Values Timedelta mod method ^^^^^^^^^^^^^^^^^^^^ -mod (%) and divmod operations are now defined on Timedelta objects when operating with either timedelta-like or with numeric arguments. (:issue:`19365`) +``mod`` (%) and ``divmod`` operations are now defined on ``Timedelta`` objects when operating with either timedelta-like or with numeric arguments. (:issue:`19365`) .. ipython:: python diff --git a/pandas/tests/scalar/test_timedelta.py b/pandas/tests/scalar/test_timedelta.py index 32dd40d6a3c6b..c245aaa8a5021 100644 --- a/pandas/tests/scalar/test_timedelta.py +++ b/pandas/tests/scalar/test_timedelta.py @@ -105,15 +105,6 @@ def test_timedelta_ops_scalar(self): result = base - offset assert result == expected_sub - def test_ops_offsets(self): - td = Timedelta(10, unit='d') - assert Timedelta(241, unit='h') == td + pd.offsets.Hour(1) - assert Timedelta(241, unit='h') == pd.offsets.Hour(1) + td - assert 240 == td / pd.offsets.Hour(1) - assert 1 / 240.0 == pd.offsets.Hour(1) / td - assert Timedelta(239, unit='h') == td - pd.offsets.Hour(1) - assert Timedelta(-239, unit='h') == pd.offsets.Hour(1) - td - def test_unary_ops(self): td = Timedelta(10, unit='d') @@ -183,6 +174,17 @@ def test_add_timedeltalike(self, op): assert isinstance(result, Timedelta) assert result == Timedelta(days=6) + def test_sub_timedeltalike(self): + td = Timedelta(10, unit='d') + + result = td - pd.offsets.Hour(1) + assert isinstance(result, Timedelta) + assert result == Timedelta(239, unit='h') + + result = pd.offsets.Hour(1) - td + assert isinstance(result, Timedelta) + assert result == Timedelta(-239, unit='h') + def test_binary_ops_nat(self): td = Timedelta(10, unit='d') @@ -224,6 +226,16 @@ def test_binary_ops_with_timedelta(self): # invalid multiply with another timedelta pytest.raises(TypeError, lambda: td * td) + def test_div(self): + td = Timedelta(10, unit='d') + result = td / pd.offsets.Hour(1) + assert result == 240 + + def test_rdiv(self): + td = Timedelta(10, unit='d') + result = pd.offsets.Hour(1) / td + assert result == 1 / 240.0 + def test_floordiv(self): # GH#18846 td = Timedelta(hours=3, minutes=4) From acff3282c9663c9b1232d35c4d64d062a94c9944 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 12 Feb 2018 08:04:36 -0800 Subject: [PATCH 08/15] implement suggested changes to catching zero-dim numpy objs --- pandas/_libs/tslibs/timedeltas.pyx | 61 +++++++++++++++--------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index b859868847035..ab21d7df9adef 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -482,24 +482,20 @@ def _binary_op_method_timedeltalike(op, name): # the PyDateTime_CheckExact case is for a datetime object that # is specifically *not* a Timestamp, as the Timestamp case will be # handled after `_validate_ops_compat` returns False below - from ..tslib import Timestamp + from timestamps import Timestamp return op(self, Timestamp(other)) # We are implicitly requiring the canonical behavior to be # defined by Timestamp methods. + elif is_timedelta64_object(other): + return op(self, Timedelta(other)) + elif hasattr(other, 'dtype'): # nd-array like if other.dtype.kind not in ['m', 'M']: # raise rathering than letting numpy return wrong answer return NotImplemented - result = op(self.to_timedelta64(), other) - if other.ndim == 0: - if other.dtype.kind == 'm': - return Timedelta(result) - if other.dtype.kind == 'M': - from ..tslib import Timestamp - return Timestamp(result) - return result + return op(self.to_timedelta64(), other) elif not _validate_ops_compat(other): return NotImplemented @@ -1051,12 +1047,14 @@ class Timedelta(_Timedelta): __rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, '__rsub__') def __mul__(self, other): + if is_integer_object(other) or is_float_object(other): + # includes numpy scalars that would otherwise be caught by dtype + # check below + return Timedelta(other * self.value, unit='ns') + if hasattr(other, 'dtype'): # ndarray-like - result = other * self.to_timedelta64() - if other.ndim == 0: - return Timedelta(result) - return result + return other * self.to_timedelta64() elif other is NaT: return NaT @@ -1070,11 +1068,11 @@ class Timedelta(_Timedelta): __rmul__ = __mul__ def __truediv__(self, other): - if hasattr(other, 'dtype'): - result = self.to_timedelta64() / other - if other.ndim == 0 and result.dtype.kind == 'm': - return Timedelta(result) - return result + if is_timedelta64_object(other): + return self / Timedelta(other) + + elif hasattr(other, 'dtype'): + return self.to_timedelta64() / other elif is_integer_object(other) or is_float_object(other): # integers or floats @@ -1089,11 +1087,11 @@ class Timedelta(_Timedelta): return self.value / float(other.value) def __rtruediv__(self, other): - if hasattr(other, 'dtype'): - result = other / self.to_timedelta64() - if other.ndim == 0 and result.dtype.kind == 'm': - return Timedelta(result) - return result + if is_timedelta64_object(other): + return Timedelta(other) / self + + elif hasattr(other, 'dtype'): + return other / self.to_timedelta64() elif not _validate_ops_compat(other): return NotImplemented @@ -1117,23 +1115,23 @@ class Timedelta(_Timedelta): return self // other.delta return NotImplemented + elif is_timedelta64_object(other): + return self // Timedelta(other) + + elif is_integer_object(other) or is_float_object(other): + return Timedelta(self.value // other, unit='ns') + if hasattr(other, 'dtype'): if other.dtype.kind == 'm': # also timedelta-like return _broadcast_floordiv_td64(self.value, other, _floordiv) elif other.dtype.kind in ['i', 'u', 'f']: - if other.ndim == 0: - return Timedelta(self.value // other) - else: - return self.to_timedelta64() // other + return self.to_timedelta64() // other raise TypeError('Invalid dtype {dtype} for ' '{op}'.format(dtype=other.dtype, op='__floordiv__')) - elif is_integer_object(other) or is_float_object(other): - return Timedelta(self.value // other, unit='ns') - elif not _validate_ops_compat(other): return NotImplemented @@ -1152,6 +1150,9 @@ class Timedelta(_Timedelta): return other.delta // self return NotImplemented + elif is_timedelta64_object(other): + return Timedelta(other) // self + if hasattr(other, 'dtype'): if other.dtype.kind == 'm': # also timedelta-like From 73fd6dc97e04b195058d4cb297545391d6e93708 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 12 Feb 2018 20:22:49 -0800 Subject: [PATCH 09/15] Delay int/float cases --- pandas/_libs/tslibs/timedeltas.pyx | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index ab21d7df9adef..82fcc14331804 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1047,13 +1047,10 @@ class Timedelta(_Timedelta): __rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, '__rsub__') def __mul__(self, other): - if is_integer_object(other) or is_float_object(other): - # includes numpy scalars that would otherwise be caught by dtype - # check below - return Timedelta(other * self.value, unit='ns') - - if hasattr(other, 'dtype'): - # ndarray-like + if (hasattr(other, 'dtype') and + not (is_integer_object(other) or is_float_object(other))): + # ndarray-like; the integer/float object checks exclude + # numpy scalars return other * self.to_timedelta64() elif other is NaT: @@ -1118,10 +1115,10 @@ class Timedelta(_Timedelta): elif is_timedelta64_object(other): return self // Timedelta(other) - elif is_integer_object(other) or is_float_object(other): - return Timedelta(self.value // other, unit='ns') - - if hasattr(other, 'dtype'): + if (hasattr(other, 'dtype') and + not (is_integer_object(other) or is_float_object(other))): + # ndarray-like; the integer/float object checks exclude + # numpy scalars if other.dtype.kind == 'm': # also timedelta-like return _broadcast_floordiv_td64(self.value, other, _floordiv) From 4cbb2e16eae1f268814041633b1526ed215f9ef5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 13 Feb 2018 07:46:20 -0800 Subject: [PATCH 10/15] revert edit that broke floordiv --- pandas/_libs/tslibs/timedeltas.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 82fcc14331804..8a7da0db3dd04 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1115,8 +1115,10 @@ class Timedelta(_Timedelta): elif is_timedelta64_object(other): return self // Timedelta(other) - if (hasattr(other, 'dtype') and - not (is_integer_object(other) or is_float_object(other))): + elif is_integer_object(other) or is_float_object(other): + return Timedelta(self.value // other, unit='ns') + + elif hasattr(other, 'dtype'): # ndarray-like; the integer/float object checks exclude # numpy scalars if other.dtype.kind == 'm': From 08fa8fd68a99f0bf903649aa5583e51465144413 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 15 Feb 2018 08:08:53 -0800 Subject: [PATCH 11/15] pass on is_timedelta64_object --- pandas/_libs/tslibs/timedeltas.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 8a7da0db3dd04..fffde1fdd008e 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -488,7 +488,8 @@ def _binary_op_method_timedeltalike(op, name): # defined by Timestamp methods. elif is_timedelta64_object(other): - return op(self, Timedelta(other)) + # other coerced to Timedelta below + pass elif hasattr(other, 'dtype'): # nd-array like From 03b7e17cc2887cce462def92c783effdcdfd6cd2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 15 Feb 2018 08:40:30 -0800 Subject: [PATCH 12/15] catch late-NaT --- pandas/_libs/tslibs/timedeltas.pyx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index fffde1fdd008e..181cf26b65bac 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -507,6 +507,9 @@ def _binary_op_method_timedeltalike(op, name): # failed to parse as timedelta return NotImplemented + if other is NaT: + # e.g. if original other was np.timedelta64('NaT') + return NaT return Timedelta(op(self.value, other.value), unit='ns') f.__name__ = name From c1fbdc9fa652186ac2176360259891921c83c003 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 16:24:22 -0800 Subject: [PATCH 13/15] requested edits to docs --- doc/source/timedeltas.rst | 4 ++-- doc/source/whatsnew/v0.23.0.txt | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/doc/source/timedeltas.rst b/doc/source/timedeltas.rst index d1e0d635996de..9890c976c4e5a 100644 --- a/doc/source/timedeltas.rst +++ b/doc/source/timedeltas.rst @@ -283,7 +283,7 @@ Rounded division (floor-division) of a ``timedelta64[ns]`` Series by a scalar td // pd.Timedelta(days=3, hours=4) pd.Timedelta(days=3, hours=4) // td -The mod (%) and divmod operations are defined for ``Timedelta`` when operating with another timedelta-like or with a numeric argument. +The mod (%) and divmod operations are defined for ``Timedelta`` when operating with another timedelta-like or with a numeric argument. (:issue:`19365`) .. ipython:: python @@ -293,7 +293,7 @@ The mod (%) and divmod operations are defined for ``Timedelta`` when operating w divmod(datetime.timedelta(hours=2), pd.Timedelta(minutes=11)) # divmod against a numeric returns a pair (Timedelta, Timedelta) - pd.Timedelta(hours=25) % 86400000000000 + divmod(pd.Timedelta(hours=25), 86400000000000) Attributes ---------- diff --git a/doc/source/whatsnew/v0.23.0.txt b/doc/source/whatsnew/v0.23.0.txt index 38dee065cf803..70ac4393da2b3 100644 --- a/doc/source/whatsnew/v0.23.0.txt +++ b/doc/source/whatsnew/v0.23.0.txt @@ -129,13 +129,6 @@ Timedelta mod method td = pd.Timedelta(hours=37) td -Current Behavior - -.. ipython:: python - - td % pd.Timedelta(hours=2) - divmod(td, np.array([2, 3], dtype='timedelta64[h]')) - .. _whatsnew_0230.enhancements.ran_inf: ``.rank()`` handles ``inf`` values when ``NaN`` are present @@ -738,7 +731,6 @@ Datetimelike - Bug in :func:`to_datetime` where passing an out-of-bounds datetime with ``errors='coerce'`` and ``utc=True`` would raise ``OutOfBoundsDatetime`` instead of parsing to ``NaT`` (:issue:`19612`) - - Timezones ^^^^^^^^^ From b2995c99c8af69685ea0104b30532c8f07587a0f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 16:45:38 -0800 Subject: [PATCH 14/15] revert zero-dim de-specialization --- pandas/_libs/tslibs/timedeltas.pyx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 181cf26b65bac..bf131f65e49b7 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1129,8 +1129,10 @@ class Timedelta(_Timedelta): # also timedelta-like return _broadcast_floordiv_td64(self.value, other, _floordiv) elif other.dtype.kind in ['i', 'u', 'f']: - return self.to_timedelta64() // other - + if other.ndim == 0: + return Timedelta(self.value // other) + else: + return self.to_timedelta64() // other raise TypeError('Invalid dtype {dtype} for ' '{op}'.format(dtype=other.dtype, op='__floordiv__')) From c22dc473e0473a5f93c9e3bc97d5b19c4ea268ea Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 16 Feb 2018 21:18:44 -0800 Subject: [PATCH 15/15] dummy commit to force CI --- pandas/_libs/tslibs/timedeltas.pyx | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index bf131f65e49b7..5a044dc7e33c1 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -1191,6 +1191,7 @@ class Timedelta(_Timedelta): return div, self - div * other def __rdivmod__(self, other): + # Naive implementation, room for optimization div = other // self return div, other - div * self