Skip to content

ENH: Timedelta/Timestamp round support non-nano #47356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions pandas/_libs/tslibs/timedeltas.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1653,15 +1653,14 @@ class Timedelta(_Timedelta):
int64_t result, unit, remainder
ndarray[int64_t] arr

if self._reso != NPY_FR_ns:
raise NotImplementedError

from pandas._libs.tslibs.offsets import to_offset
unit = to_offset(freq).nanos

to_offset(freq).nanos # raises on non-fixed freq
unit = delta_to_nanoseconds(to_offset(freq), self._reso)

arr = np.array([self.value], dtype="i8")
result = round_nsint64(arr, mode, unit)[0]
return Timedelta(result, unit="ns")
return Timedelta._from_value_and_reso(result, self._reso)

def round(self, freq):
"""
Expand Down
8 changes: 4 additions & 4 deletions pandas/_libs/tslibs/timestamps.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -1645,10 +1645,10 @@ class Timestamp(_Timestamp):

def _round(self, freq, mode, ambiguous='raise', nonexistent='raise'):
cdef:
int64_t nanos = to_offset(freq).nanos
int64_t nanos

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

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

# Will only ever contain 1 element for timestamp
r = round_nsint64(value, mode, nanos)[0]
result = Timestamp(r, unit='ns')
result = Timestamp._from_value_and_reso(r, self._reso, None)
if self.tz is not None:
result = result.tz_localize(
self.tz, ambiguous=ambiguous, nonexistent=nonexistent
Expand Down
3 changes: 2 additions & 1 deletion pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -1933,7 +1933,8 @@ def _round(self, freq, mode, ambiguous, nonexistent):

values = self.view("i8")
values = cast(np.ndarray, values)
nanos = to_offset(freq).nanos
nanos = to_offset(freq).nanos # raises on non-fixed frequencies
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have tests that trigger the raising behavior?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

nanos = delta_to_nanoseconds(to_offset(freq), self._reso)
result_i8 = round_nsint64(values, mode, nanos)
result = self._maybe_mask_results(result_i8, fill_value=iNaT)
result = result.view(self._ndarray.dtype)
Expand Down
16 changes: 16 additions & 0 deletions pandas/tests/scalar/timedelta/test_timedelta.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,22 @@ def test_round_sanity(self, val, method):
assert np.abs((res - td).value) < nanos
assert res.value % nanos == 0

@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
def test_round_non_nano(self, unit):
td = Timedelta("1 days 02:34:57")._as_unit(unit)

res = td.round("min")
assert res == Timedelta("1 days 02:35:00")
assert res._reso == td._reso

res = td.floor("min")
assert res == Timedelta("1 days 02:34:00")
assert res._reso == td._reso

res = td.ceil("min")
assert res == Timedelta("1 days 02:35:00")
assert res._reso == td._reso

def test_contains(self):
# Checking for any NaT-like objects
# GH 13603
Expand Down
26 changes: 10 additions & 16 deletions pandas/tests/scalar/timestamp/test_unary_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,27 +148,26 @@ def test_round_minute_freq(self, test_input, freq, expected, rounder):
result = func(freq)
assert result == expected

def test_ceil(self):
dt = Timestamp("20130101 09:10:11")
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you parameterize test_ceil below too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea, updated

def test_ceil(self, unit):
dt = Timestamp("20130101 09:10:11")._as_unit(unit)
result = dt.ceil("D")
expected = Timestamp("20130102")
assert result == expected
assert result._reso == dt._reso

def test_floor(self):
dt = Timestamp("20130101 09:10:11")
@pytest.mark.parametrize("unit", ["ns", "us", "ms", "s"])
def test_floor(self, unit):
dt = Timestamp("20130101 09:10:11")._as_unit(unit)
result = dt.floor("D")
expected = Timestamp("20130101")
assert result == expected
assert result._reso == dt._reso

@pytest.mark.parametrize("method", ["ceil", "round", "floor"])
@pytest.mark.parametrize(
"unit",
[
"ns",
pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")),
pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")),
pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")),
],
["ns", "us", "ms", "s"],
)
def test_round_dst_border_ambiguous(self, method, unit):
# GH 18946 round near "fall back" DST
Expand Down Expand Up @@ -203,12 +202,7 @@ def test_round_dst_border_ambiguous(self, method, unit):
)
@pytest.mark.parametrize(
"unit",
[
"ns",
pytest.param("us", marks=pytest.mark.xfail(reason="round not implemented")),
pytest.param("ms", marks=pytest.mark.xfail(reason="round not implemented")),
pytest.param("s", marks=pytest.mark.xfail(reason="round not implemented")),
],
["ns", "us", "ms", "s"],
)
def test_round_dst_border_nonexistent(self, method, ts_str, freq, unit):
# GH 23324 round near "spring forward" DST
Expand Down