Skip to content

Commit 65b23c2

Browse files
jbrockmendeljreback
authored andcommitted
BUG: partial-timestamp slicing near the end of year/quarter/month (#31064)
1 parent 1c9f23c commit 65b23c2

File tree

3 files changed

+38
-11
lines changed

3 files changed

+38
-11
lines changed

doc/source/whatsnew/v1.1.0.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Interval
102102

103103
Indexing
104104
^^^^^^^^
105-
105+
- Bug in slicing on a :class:`DatetimeIndex` with a partial-timestamp dropping high-resolution indices near the end of a year, quarter, or month (:issue:`31064`)
106106
-
107107
-
108108

pandas/core/indexes/datetimes.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55

66
import numpy as np
77

8-
from pandas._libs import NaT, Timestamp, index as libindex, lib, tslib as libts
8+
from pandas._libs import (
9+
NaT,
10+
Timedelta,
11+
Timestamp,
12+
index as libindex,
13+
lib,
14+
tslib as libts,
15+
)
916
from pandas._libs.tslibs import ccalendar, fields, parsing, timezones
1017
from pandas.util._decorators import Appender, Substitution, cache_readonly
1118

@@ -31,7 +38,7 @@
3138
import pandas.core.tools.datetimes as tools
3239

3340
from pandas.tseries.frequencies import Resolution, to_offset
34-
from pandas.tseries.offsets import Nano, prefix_mapping
41+
from pandas.tseries.offsets import prefix_mapping
3542

3643

3744
def _new_DatetimeIndex(cls, d):
@@ -519,27 +526,27 @@ def _parsed_string_to_bounds(self, reso, parsed):
519526
raise KeyError
520527
if reso == "year":
521528
start = Timestamp(parsed.year, 1, 1)
522-
end = Timestamp(parsed.year, 12, 31, 23, 59, 59, 999999)
529+
end = Timestamp(parsed.year + 1, 1, 1) - Timedelta(nanoseconds=1)
523530
elif reso == "month":
524531
d = ccalendar.get_days_in_month(parsed.year, parsed.month)
525532
start = Timestamp(parsed.year, parsed.month, 1)
526-
end = Timestamp(parsed.year, parsed.month, d, 23, 59, 59, 999999)
533+
end = start + Timedelta(days=d, nanoseconds=-1)
527534
elif reso == "quarter":
528535
qe = (((parsed.month - 1) + 2) % 12) + 1 # two months ahead
529536
d = ccalendar.get_days_in_month(parsed.year, qe) # at end of month
530537
start = Timestamp(parsed.year, parsed.month, 1)
531-
end = Timestamp(parsed.year, qe, d, 23, 59, 59, 999999)
538+
end = Timestamp(parsed.year, qe, 1) + Timedelta(days=d, nanoseconds=-1)
532539
elif reso == "day":
533540
start = Timestamp(parsed.year, parsed.month, parsed.day)
534-
end = start + timedelta(days=1) - Nano(1)
541+
end = start + Timedelta(days=1, nanoseconds=-1)
535542
elif reso == "hour":
536543
start = Timestamp(parsed.year, parsed.month, parsed.day, parsed.hour)
537-
end = start + timedelta(hours=1) - Nano(1)
544+
end = start + Timedelta(hours=1, nanoseconds=-1)
538545
elif reso == "minute":
539546
start = Timestamp(
540547
parsed.year, parsed.month, parsed.day, parsed.hour, parsed.minute
541548
)
542-
end = start + timedelta(minutes=1) - Nano(1)
549+
end = start + Timedelta(minutes=1, nanoseconds=-1)
543550
elif reso == "second":
544551
start = Timestamp(
545552
parsed.year,
@@ -549,7 +556,7 @@ def _parsed_string_to_bounds(self, reso, parsed):
549556
parsed.minute,
550557
parsed.second,
551558
)
552-
end = start + timedelta(seconds=1) - Nano(1)
559+
end = start + Timedelta(seconds=1, nanoseconds=-1)
553560
elif reso == "microsecond":
554561
start = Timestamp(
555562
parsed.year,
@@ -560,7 +567,7 @@ def _parsed_string_to_bounds(self, reso, parsed):
560567
parsed.second,
561568
parsed.microsecond,
562569
)
563-
end = start + timedelta(microseconds=1) - Nano(1)
570+
end = start + Timedelta(microseconds=1, nanoseconds=-1)
564571
# GH 24076
565572
# If an incoming date string contained a UTC offset, need to localize
566573
# the parsed date to this offset first before aligning with the index's

pandas/tests/indexes/datetimes/test_partial_slicing.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,26 @@ def test_slice_year(self):
142142
expected = slice(3288, 3653)
143143
assert result == expected
144144

145+
@pytest.mark.parametrize(
146+
"partial_dtime",
147+
[
148+
"2019",
149+
"2019Q4",
150+
"Dec 2019",
151+
"2019-12-31",
152+
"2019-12-31 23",
153+
"2019-12-31 23:59",
154+
],
155+
)
156+
def test_slice_end_of_period_resolution(self, partial_dtime):
157+
# GH#31064
158+
dti = date_range("2019-12-31 23:59:55.999999999", periods=10, freq="s")
159+
160+
ser = pd.Series(range(10), index=dti)
161+
result = ser[partial_dtime]
162+
expected = ser.iloc[:5]
163+
tm.assert_series_equal(result, expected)
164+
145165
def test_slice_quarter(self):
146166
dti = date_range(freq="D", start=datetime(2000, 6, 1), periods=500)
147167

0 commit comments

Comments
 (0)