Skip to content

Commit b47af42

Browse files
committed
BUG: slicing with Timestamp with mismatched unit
1 parent 8be8439 commit b47af42

File tree

3 files changed

+47
-1
lines changed

3 files changed

+47
-1
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,7 @@ Indexing
11951195
- Bug in :meth:`Index.get_indexer` and similar methods when ``NaN`` is located at or after position 128 (:issue:`58924`)
11961196
- Bug in :meth:`MultiIndex.insert` when a new value inserted to a datetime-like level gets cast to ``NaT`` and fails indexing (:issue:`60388`)
11971197
- Bug in :meth:`Series.__setitem__` when assigning boolean series with boolean indexer will raise ``LossySetitemError`` (:issue:`57338`)
1198+
- Bug in indexing ``obj.loc[start:stop]`` with a :class:`DatetimeIndex` and :class:`Timestamp` endpoints with higher resolution than the index (:issue:`63262`)
11981199
- Bug in printing :attr:`Index.names` and :attr:`MultiIndex.levels` would not escape single quotes (:issue:`60190`)
11991200
- Bug in reindexing of :class:`DataFrame` with :class:`PeriodDtype` columns in case of consolidated block (:issue:`60980`, :issue:`60273`)
12001201
- Bug in :meth:`DataFrame.loc.__getitem__` and :meth:`DataFrame.iloc.__getitem__` with a :class:`CategoricalDtype` column with integer categories raising when trying to index a row containing a ``NaN`` entry (:issue:`58954`)

pandas/core/indexes/datetimelike.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import (
1212
TYPE_CHECKING,
1313
Any,
14+
Literal,
1415
Self,
1516
cast,
1617
final,
@@ -20,16 +21,18 @@
2021

2122
from pandas._libs import (
2223
NaT,
23-
Timedelta,
2424
lib,
2525
)
2626
from pandas._libs.tslibs import (
2727
BaseOffset,
2828
Resolution,
2929
Tick,
30+
Timedelta,
31+
Timestamp,
3032
parsing,
3133
to_offset,
3234
)
35+
from pandas._libs.tslibs.dtypes import abbrev_to_npy_unit
3336
from pandas.compat.numpy import function as nv
3437
from pandas.errors import (
3538
InvalidIndexError,
@@ -933,6 +936,22 @@ def _from_join_target(self, result: np.ndarray):
933936
result = result.view(self._data._ndarray.dtype)
934937
return self._data._from_backing_data(result)
935938

939+
def _searchsorted_monotonic(self, label, side: Literal["left", "right"] = "left"):
940+
if (
941+
self.is_monotonic_increasing
942+
and isinstance(label, (Timestamp, Timedelta))
943+
and abbrev_to_npy_unit(label.unit) > abbrev_to_npy_unit(self.unit)
944+
):
945+
# For non-matching units we can safely round down (with side=right)
946+
# This is needed for GH#63262
947+
if side == "right":
948+
label = label.as_unit(self.unit) # this should always be a round-down
949+
else:
950+
# round up
951+
label = label.ceil(self.unit).as_unit(self.unit)
952+
953+
return super()._searchsorted_monotonic(label, side)
954+
936955
# --------------------------------------------------------------------
937956
# List-like Methods
938957

pandas/tests/indexes/datetimes/test_indexing.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,32 @@ def test_maybe_cast_slice_duplicate_monotonic(self):
656656

657657

658658
class TestGetSliceBounds:
659+
@pytest.mark.parametrize("as_td", [True, False])
660+
def test_get_slice_bound_mismatched_unit(self, as_td):
661+
# GH#63262
662+
index = date_range(start="2000-01-01", freq="h", periods=8)
663+
664+
td = pd.Timedelta(1)
665+
ts = Timestamp("2000-01-01 01:00:00")
666+
start = ts - td
667+
stop = ts + td
668+
if as_td:
669+
index = index - Timestamp(0).as_unit("us")
670+
start = start - Timestamp(0).as_unit("us")
671+
stop = stop - Timestamp(0).as_unit("us")
672+
673+
left = index.get_slice_bound(start, side="left")
674+
assert left == 1
675+
right = index.get_slice_bound(stop, side="right")
676+
assert right == 2
677+
678+
# The user-facing behavior is slicing with .loc, so let's test that
679+
# explicitly while we're here.
680+
ser = pd.Series(1, index=index)
681+
result = ser.loc[start:stop]
682+
expected = ser.iloc[1:2]
683+
tm.assert_series_equal(result, expected)
684+
659685
@pytest.mark.parametrize("box", [date, datetime, Timestamp])
660686
@pytest.mark.parametrize("side, expected", [("left", 4), ("right", 5)])
661687
def test_get_slice_bounds_datetime_within(

0 commit comments

Comments
 (0)