Skip to content

Commit 32261ce

Browse files
jbrockmendelJulianWgs
authored andcommitted
BUG: wrong exception when slicing on TimedeltaIndex (pandas-dev#41821)
1 parent f3b4758 commit 32261ce

File tree

5 files changed

+70
-12
lines changed

5 files changed

+70
-12
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@ Indexing
10251025
- Bug in :meth:`PeriodIndex.get_loc` failing to raise a ``KeyError`` when given a :class:`Period` with a mismatched ``freq`` (:issue:`41670`)
10261026
- Bug ``.loc.__getitem__`` with a :class:`UInt64Index` and negative-integer keys raising ``OverflowError`` instead of ``KeyError`` in some cases, wrapping around to positive integers in others (:issue:`41777`)
10271027
- Bug in :meth:`Index.get_indexer` failing to raise ``ValueError`` in some cases with invalid ``method``, ``limit``, or ``tolerance`` arguments (:issue:`41918`)
1028+
- Bug when slicing a :class:`Series` or :class:`DataFrame` with a :class:`TimedeltaIndex` when passing an invalid string raising ``ValueError`` instead of a ``TypeError`` (:issue:`41821`)
10281029

10291030
Missing
10301031
^^^^^^^

pandas/core/indexes/datetimelike.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
Resolution,
2727
Tick,
2828
)
29-
from pandas._typing import Callable
29+
from pandas._typing import (
30+
Callable,
31+
final,
32+
)
3033
from pandas.compat.numpy import function as nv
3134
from pandas.util._decorators import (
3235
Appender,
@@ -400,6 +403,7 @@ def _validate_partial_date_slice(self, reso: Resolution):
400403
def _parsed_string_to_bounds(self, reso: Resolution, parsed: datetime):
401404
raise NotImplementedError
402405

406+
@final
403407
def _partial_date_slice(
404408
self,
405409
reso: Resolution,

pandas/core/indexes/period.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131

3232
from pandas.core.dtypes.common import (
3333
is_datetime64_any_dtype,
34-
is_float,
3534
is_integer,
3635
pandas_dtype,
3736
)
@@ -489,13 +488,14 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
489488
elif isinstance(label, str):
490489
try:
491490
parsed, reso_str = parse_time_string(label, self.freq)
492-
reso = Resolution.from_attrname(reso_str)
493-
bounds = self._parsed_string_to_bounds(reso, parsed)
494-
return bounds[0 if side == "left" else 1]
495491
except ValueError as err:
496492
# string cannot be parsed as datetime-like
497493
raise self._invalid_indexer("slice", label) from err
498-
elif is_integer(label) or is_float(label):
494+
495+
reso = Resolution.from_attrname(reso_str)
496+
lower, upper = self._parsed_string_to_bounds(reso, parsed)
497+
return lower if side == "left" else upper
498+
elif not isinstance(label, self._data._recognized_scalars):
499499
raise self._invalid_indexer("slice", label)
500500

501501
return label

pandas/core/indexes/timedeltas.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,16 @@ def _maybe_cast_slice_bound(self, label, side: str, kind=lib.no_default):
196196
self._deprecated_arg(kind, "kind", "_maybe_cast_slice_bound")
197197

198198
if isinstance(label, str):
199-
parsed = Timedelta(label)
200-
lbound = parsed.round(parsed.resolution_string)
201-
if side == "left":
202-
return lbound
203-
else:
204-
return lbound + to_offset(parsed.resolution_string) - Timedelta(1, "ns")
199+
try:
200+
parsed = Timedelta(label)
201+
except ValueError as err:
202+
# e.g. 'unit abbreviation w/o a number'
203+
raise self._invalid_indexer("slice", label) from err
204+
205+
# The next two lines are analogous to DTI/PI._parsed_str_to_bounds
206+
lower = parsed.round(parsed.resolution_string)
207+
upper = lower + to_offset(parsed.resolution_string) - Timedelta(1, "ns")
208+
return lower if side == "left" else upper
205209
elif not isinstance(label, self._data._recognized_scalars):
206210
raise self._invalid_indexer("slice", label)
207211

pandas/tests/indexes/timedeltas/test_indexing.py

+49
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,52 @@ def test_take_fill_value(self):
291291
msg = "index -5 is out of bounds for (axis 0 with )?size 3"
292292
with pytest.raises(IndexError, match=msg):
293293
idx.take(np.array([1, -5]))
294+
295+
296+
class TestMaybeCastSliceBound:
297+
@pytest.fixture(params=["increasing", "decreasing", None])
298+
def monotonic(self, request):
299+
return request.param
300+
301+
@pytest.fixture
302+
def tdi(self, monotonic):
303+
tdi = timedelta_range("1 Day", periods=10)
304+
if monotonic == "decreasing":
305+
tdi = tdi[::-1]
306+
elif monotonic is None:
307+
taker = np.arange(10, dtype=np.intp)
308+
np.random.shuffle(taker)
309+
tdi = tdi.take(taker)
310+
return tdi
311+
312+
def test_maybe_cast_slice_bound_invalid_str(self, tdi):
313+
# test the low-level _maybe_cast_slice_bound and that we get the
314+
# expected exception+message all the way up the stack
315+
msg = (
316+
"cannot do slice indexing on TimedeltaIndex with these "
317+
r"indexers \[foo\] of type str"
318+
)
319+
with pytest.raises(TypeError, match=msg):
320+
tdi._maybe_cast_slice_bound("foo", side="left")
321+
with pytest.raises(TypeError, match=msg):
322+
tdi.get_slice_bound("foo", side="left")
323+
with pytest.raises(TypeError, match=msg):
324+
tdi.slice_locs("foo", None, None)
325+
326+
def test_slice_invalid_str_with_timedeltaindex(
327+
self, tdi, frame_or_series, indexer_sl
328+
):
329+
obj = frame_or_series(range(10), index=tdi)
330+
331+
msg = (
332+
"cannot do slice indexing on TimedeltaIndex with these "
333+
r"indexers \[foo\] of type str"
334+
)
335+
with pytest.raises(TypeError, match=msg):
336+
indexer_sl(obj)["foo":]
337+
with pytest.raises(TypeError, match=msg):
338+
indexer_sl(obj)["foo":-1]
339+
with pytest.raises(TypeError, match=msg):
340+
indexer_sl(obj)[:"foo"]
341+
with pytest.raises(TypeError, match=msg):
342+
indexer_sl(obj)[tdi[0] : "foo"]

0 commit comments

Comments
 (0)