From b88f997464dcbc132848cc9d717990589a42a0c1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 12 Jan 2020 16:26:00 -0800 Subject: [PATCH 1/3] REF: handle searchsorted casting within DatetimeLikeArray --- pandas/core/arrays/datetimelike.py | 27 +++++++++++++++++++---- pandas/core/indexes/datetimes.py | 17 +++----------- pandas/core/indexes/period.py | 12 ---------- pandas/core/indexes/timedeltas.py | 17 +++----------- pandas/tests/arrays/test_datetimes.py | 20 ++++++----------- pandas/tests/arrays/test_timedeltas.py | 20 ++++++----------- pandas/tests/indexes/period/test_tools.py | 7 +++++- 7 files changed, 49 insertions(+), 71 deletions(-) diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index d7c508c890a46..70637026c278d 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -743,17 +743,36 @@ def searchsorted(self, value, side="left", sorter=None): Array of insertion points with the same shape as `value`. """ if isinstance(value, str): - value = self._scalar_from_string(value) + try: + value = self._scalar_from_string(value) + except ValueError: + raise TypeError("searchsorted requires compatible dtype or scalar") + + elif is_valid_nat_for_dtype(value, self.dtype): + value = NaT + + elif isinstance(value, self._recognized_scalars): + value = self._scalar_type(value) + + elif isinstance(value, np.ndarray): + if not type(self)._is_recognized_dtype(value): + raise TypeError( + "searchsorted requires compatible dtype or scalar, " + f"not {type(value).__name__}" + ) + value = type(self)(value) + self._check_compatible_with(value) - if not (isinstance(value, (self._scalar_type, type(self))) or isna(value)): - raise ValueError(f"Unexpected type for 'value': {type(value)}") + if not (isinstance(value, (self._scalar_type, type(self))) or (value is NaT)): + raise TypeError(f"Unexpected type for 'value': {type(value)}") - self._check_compatible_with(value) if isinstance(value, type(self)): + self._check_compatible_with(value) value = value.asi8 else: value = self._unbox_scalar(value) + # TODO: Use datetime64 semantics for sorting, xref GH#29844 return self.asi8.searchsorted(value, side=side, sorter=sorter) def repeat(self, repeats, *args, **kwargs): diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 2241921e94694..88d3e5cbb9aca 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -835,24 +835,13 @@ def slice_indexer(self, start=None, end=None, step=None, kind=None): @Substitution(klass="DatetimeIndex") @Appender(_shared_docs["searchsorted"]) def searchsorted(self, value, side="left", sorter=None): - if isinstance(value, (np.ndarray, Index)): - if not type(self._data)._is_recognized_dtype(value): - raise TypeError( - "searchsorted requires compatible dtype or scalar, " - f"not {type(value).__name__}" - ) - value = type(self._data)(value) - self._data._check_compatible_with(value) - - elif isinstance(value, self._data._recognized_scalars): - self._data._check_compatible_with(value) - value = self._data._scalar_type(value) - - elif not isinstance(value, DatetimeArray): + if isinstance(value, str): raise TypeError( "searchsorted requires compatible dtype or scalar, " f"not {type(value).__name__}" ) + if isinstance(value, Index): + value = value._data return self._data.searchsorted(value, side=side) diff --git a/pandas/core/indexes/period.py b/pandas/core/indexes/period.py index 6ab2e66e05d6e..dd3819bffe2b1 100644 --- a/pandas/core/indexes/period.py +++ b/pandas/core/indexes/period.py @@ -469,18 +469,6 @@ def astype(self, dtype, copy=True, how="start"): @Substitution(klass="PeriodIndex") @Appender(_shared_docs["searchsorted"]) def searchsorted(self, value, side="left", sorter=None): - if isinstance(value, Period) or value is NaT: - self._data._check_compatible_with(value) - elif isinstance(value, str): - try: - value = Period(value, freq=self.freq) - except DateParseError: - raise KeyError(f"Cannot interpret '{value}' as period") - elif not isinstance(value, PeriodArray): - raise TypeError( - "PeriodIndex.searchsorted requires either a Period or PeriodArray" - ) - return self._data.searchsorted(value, side=side, sorter=sorter) @property diff --git a/pandas/core/indexes/timedeltas.py b/pandas/core/indexes/timedeltas.py index 1f3182bc83e1d..f5ffe07c53b0d 100644 --- a/pandas/core/indexes/timedeltas.py +++ b/pandas/core/indexes/timedeltas.py @@ -347,24 +347,13 @@ def _partial_td_slice(self, key): @Substitution(klass="TimedeltaIndex") @Appender(_shared_docs["searchsorted"]) def searchsorted(self, value, side="left", sorter=None): - if isinstance(value, (np.ndarray, Index)): - if not type(self._data)._is_recognized_dtype(value): - raise TypeError( - "searchsorted requires compatible dtype or scalar, " - f"not {type(value).__name__}" - ) - value = type(self._data)(value) - self._data._check_compatible_with(value) - - elif isinstance(value, self._data._recognized_scalars): - self._data._check_compatible_with(value) - value = self._data._scalar_type(value) - - elif not isinstance(value, TimedeltaArray): + if isinstance(value, str): raise TypeError( "searchsorted requires compatible dtype or scalar, " f"not {type(value).__name__}" ) + if isinstance(value, Index): + value = value._data return self._data.searchsorted(value, side=side, sorter=sorter) diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index 5608ab5fbd9db..a59ed429cc404 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -331,25 +331,19 @@ def test_searchsorted_tzawareness_compat(self, index): pd.Timestamp.now().to_period("D"), ], ) - @pytest.mark.parametrize( - "index", - [ - True, - pytest.param( - False, - marks=pytest.mark.xfail( - reason="Raises ValueError instead of TypeError", raises=ValueError - ), - ), - ], - ) + @pytest.mark.parametrize("index", [True, False]) def test_searchsorted_invalid_types(self, other, index): data = np.arange(10, dtype="i8") * 24 * 3600 * 10 ** 9 arr = DatetimeArray(data, freq="D") if index: arr = pd.Index(arr) - msg = "searchsorted requires compatible dtype or scalar" + msg = "|".join( + [ + "searchsorted requires compatible dtype or scalar", + "Unexpected type for 'value'", + ] + ) with pytest.raises(TypeError, match=msg): arr.searchsorted(other) diff --git a/pandas/tests/arrays/test_timedeltas.py b/pandas/tests/arrays/test_timedeltas.py index 62cb4766171a4..c86b4f71ee592 100644 --- a/pandas/tests/arrays/test_timedeltas.py +++ b/pandas/tests/arrays/test_timedeltas.py @@ -154,25 +154,19 @@ def test_setitem_objects(self, obj): pd.Timestamp.now().to_period("D"), ], ) - @pytest.mark.parametrize( - "index", - [ - True, - pytest.param( - False, - marks=pytest.mark.xfail( - reason="Raises ValueError instead of TypeError", raises=ValueError - ), - ), - ], - ) + @pytest.mark.parametrize("index", [True, False]) def test_searchsorted_invalid_types(self, other, index): data = np.arange(10, dtype="i8") * 24 * 3600 * 10 ** 9 arr = TimedeltaArray(data, freq="D") if index: arr = pd.Index(arr) - msg = "searchsorted requires compatible dtype or scalar" + msg = "|".join( + [ + "searchsorted requires compatible dtype or scalar", + "Unexpected type for 'value'", + ] + ) with pytest.raises(TypeError, match=msg): arr.searchsorted(other) diff --git a/pandas/tests/indexes/period/test_tools.py b/pandas/tests/indexes/period/test_tools.py index 28ab14af71362..23350fdff4b78 100644 --- a/pandas/tests/indexes/period/test_tools.py +++ b/pandas/tests/indexes/period/test_tools.py @@ -249,7 +249,12 @@ def test_searchsorted_invalid(self): other = np.array([0, 1], dtype=np.int64) - msg = "requires either a Period or PeriodArray" + msg = "|".join( + [ + "searchsorted requires compatible dtype or scalar", + "Unexpected type for 'value'", + ] + ) with pytest.raises(TypeError, match=msg): pidx.searchsorted(other) From 14346665209beb463b7664fa858b2985b884c3f7 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 15 Jan 2020 08:57:00 -0800 Subject: [PATCH 2/3] Whatsnew --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index b5a7b19f160a4..04dd626d84f33 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -60,7 +60,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ - Bug in :class:`Timestamp` where constructing :class:`Timestamp` from ambiguous epoch time and calling constructor again changed :meth:`Timestamp.value` property (:issue:`24329`) -- +- :meth:`DatetimeArray.searchsorted`, :meth:`TimedeltaArray.searchsorted`, :meth:`PeriodArray.searcshorted` not recognizing non-pandas scalars and incorrectly raising ``ValueError`` instead of ``TypeError`` (:issue:`30950`) - Timedelta From 24682dc7d6d521f82f1ac30978e7ed9004cb1c85 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 15 Jan 2020 10:37:42 -0800 Subject: [PATCH 3/3] Update doc/source/whatsnew/v1.1.0.rst Co-Authored-By: Simon Hawkins --- doc/source/whatsnew/v1.1.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 04dd626d84f33..b6121cfabba7e 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -60,7 +60,7 @@ Categorical Datetimelike ^^^^^^^^^^^^ - Bug in :class:`Timestamp` where constructing :class:`Timestamp` from ambiguous epoch time and calling constructor again changed :meth:`Timestamp.value` property (:issue:`24329`) -- :meth:`DatetimeArray.searchsorted`, :meth:`TimedeltaArray.searchsorted`, :meth:`PeriodArray.searcshorted` not recognizing non-pandas scalars and incorrectly raising ``ValueError`` instead of ``TypeError`` (:issue:`30950`) +- :meth:`DatetimeArray.searchsorted`, :meth:`TimedeltaArray.searchsorted`, :meth:`PeriodArray.searchsorted` not recognizing non-pandas scalars and incorrectly raising ``ValueError`` instead of ``TypeError`` (:issue:`30950`) - Timedelta