Skip to content

Commit c6e7d8d

Browse files
authored
ENH: Timedelta/Timestamp round support non-nano (#47356)
* ENH: Timedelta/Timestamp round support non-nano * parametrize test_floor * un-xfail
1 parent 18124f9 commit c6e7d8d

File tree

5 files changed

+36
-26
lines changed

5 files changed

+36
-26
lines changed

pandas/_libs/tslibs/timedeltas.pyx

+4-5
Original file line numberDiff line numberDiff line change
@@ -1653,15 +1653,14 @@ class Timedelta(_Timedelta):
16531653
int64_t result, unit, remainder
16541654
ndarray[int64_t] arr
16551655

1656-
if self._reso != NPY_FR_ns:
1657-
raise NotImplementedError
1658-
16591656
from pandas._libs.tslibs.offsets import to_offset
1660-
unit = to_offset(freq).nanos
1657+
1658+
to_offset(freq).nanos # raises on non-fixed freq
1659+
unit = delta_to_nanoseconds(to_offset(freq), self._reso)
16611660

16621661
arr = np.array([self.value], dtype="i8")
16631662
result = round_nsint64(arr, mode, unit)[0]
1664-
return Timedelta(result, unit="ns")
1663+
return Timedelta._from_value_and_reso(result, self._reso)
16651664

16661665
def round(self, freq):
16671666
"""

pandas/_libs/tslibs/timestamps.pyx

+4-4
Original file line numberDiff line numberDiff line change
@@ -1645,10 +1645,10 @@ class Timestamp(_Timestamp):
16451645

16461646
def _round(self, freq, mode, ambiguous='raise', nonexistent='raise'):
16471647
cdef:
1648-
int64_t nanos = to_offset(freq).nanos
1648+
int64_t nanos
16491649

1650-
if self._reso != NPY_FR_ns:
1651-
raise NotImplementedError(self._reso)
1650+
to_offset(freq).nanos # raises on non-fixed freq
1651+
nanos = delta_to_nanoseconds(to_offset(freq), self._reso)
16521652

16531653
if self.tz is not None:
16541654
value = self.tz_localize(None).value
@@ -1659,7 +1659,7 @@ class Timestamp(_Timestamp):
16591659

16601660
# Will only ever contain 1 element for timestamp
16611661
r = round_nsint64(value, mode, nanos)[0]
1662-
result = Timestamp(r, unit='ns')
1662+
result = Timestamp._from_value_and_reso(r, self._reso, None)
16631663
if self.tz is not None:
16641664
result = result.tz_localize(
16651665
self.tz, ambiguous=ambiguous, nonexistent=nonexistent

pandas/core/arrays/datetimelike.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1933,7 +1933,8 @@ def _round(self, freq, mode, ambiguous, nonexistent):
19331933

19341934
values = self.view("i8")
19351935
values = cast(np.ndarray, values)
1936-
nanos = to_offset(freq).nanos
1936+
nanos = to_offset(freq).nanos # raises on non-fixed frequencies
1937+
nanos = delta_to_nanoseconds(to_offset(freq), self._reso)
19371938
result_i8 = round_nsint64(values, mode, nanos)
19381939
result = self._maybe_mask_results(result_i8, fill_value=iNaT)
19391940
result = result.view(self._ndarray.dtype)

pandas/tests/scalar/timedelta/test_timedelta.py

+16
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,22 @@ def test_round_sanity(self, val, method):
661661
assert np.abs((res - td).value) < nanos
662662
assert res.value % nanos == 0
663663

664+
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
665+
def test_round_non_nano(self, unit):
666+
td = Timedelta("1 days 02:34:57")._as_unit(unit)
667+
668+
res = td.round("min")
669+
assert res == Timedelta("1 days 02:35:00")
670+
assert res._reso == td._reso
671+
672+
res = td.floor("min")
673+
assert res == Timedelta("1 days 02:34:00")
674+
assert res._reso == td._reso
675+
676+
res = td.ceil("min")
677+
assert res == Timedelta("1 days 02:35:00")
678+
assert res._reso == td._reso
679+
664680
def test_contains(self):
665681
# Checking for any NaT-like objects
666682
# GH 13603

pandas/tests/scalar/timestamp/test_unary_ops.py

+10-16
Original file line numberDiff line numberDiff line change
@@ -148,27 +148,26 @@ def test_round_minute_freq(self, test_input, freq, expected, rounder):
148148
result = func(freq)
149149
assert result == expected
150150

151-
def test_ceil(self):
152-
dt = Timestamp("20130101 09:10:11")
151+
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
152+
def test_ceil(self, unit):
153+
dt = Timestamp("20130101 09:10:11")._as_unit(unit)
153154
result = dt.ceil("D")
154155
expected = Timestamp("20130102")
155156
assert result == expected
157+
assert result._reso == dt._reso
156158

157-
def test_floor(self):
158-
dt = Timestamp("20130101 09:10:11")
159+
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
160+
def test_floor(self, unit):
161+
dt = Timestamp("20130101 09:10:11")._as_unit(unit)
159162
result = dt.floor("D")
160163
expected = Timestamp("20130101")
161164
assert result == expected
165+
assert result._reso == dt._reso
162166

163167
@pytest.mark.parametrize("method", ["ceil", "round", "floor"])
164168
@pytest.mark.parametrize(
165169
"unit",
166-
[
167-
"ns",
168-
pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")),
169-
pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")),
170-
pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")),
171-
],
170+
["ns", "us", "ms", "s"],
172171
)
173172
def test_round_dst_border_ambiguous(self, method, unit):
174173
# GH 18946 round near "fall back" DST
@@ -203,12 +202,7 @@ def test_round_dst_border_ambiguous(self, method, unit):
203202
)
204203
@pytest.mark.parametrize(
205204
"unit",
206-
[
207-
"ns",
208-
pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")),
209-
pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")),
210-
pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")),
211-
],
205+
["ns", "us", "ms", "s"],
212206
)
213207
def test_round_dst_border_nonexistent(self, method, ts_str, freq, unit):
214208
# GH 23324 round near "spring forward" DST

0 commit comments

Comments
 (0)