Skip to content

Commit e1df797

Browse files
authored
ENH: implement Timestamp _as_reso, _as_unit (#47333)
1 parent 5465f54 commit e1df797

File tree

5 files changed

+128
-6
lines changed

5 files changed

+128
-6
lines changed

pandas/_libs/tslibs/timestamps.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,4 @@ cdef class _Timestamp(ABCTimestamp):
3737
cpdef void _set_freq(self, freq)
3838
cdef _warn_on_field_deprecation(_Timestamp self, freq, str field)
3939
cdef bint _compare_mismatched_resos(_Timestamp self, _Timestamp other, int op)
40+
cdef _Timestamp _as_reso(_Timestamp self, NPY_DATETIMEUNIT reso, bint round_ok=*)

pandas/_libs/tslibs/timestamps.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,4 @@ class Timestamp(datetime):
222222
def days_in_month(self) -> int: ...
223223
@property
224224
def daysinmonth(self) -> int: ...
225+
def _as_unit(self, unit: str, round_ok: bool = ...) -> Timestamp: ...

pandas/_libs/tslibs/timestamps.pyx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ from pandas._libs.tslibs.conversion cimport (
5454
maybe_localize_tso,
5555
)
5656
from pandas._libs.tslibs.dtypes cimport (
57+
get_conversion_factor,
5758
npy_unit_to_abbrev,
5859
periods_per_day,
5960
periods_per_second,
@@ -86,6 +87,7 @@ from pandas._libs.tslibs.np_datetime cimport (
8687
dt64_to_dtstruct,
8788
get_datetime64_unit,
8889
get_datetime64_value,
90+
get_unit_from_dtype,
8991
npy_datetimestruct,
9092
pandas_datetime_to_datetimestruct,
9193
pydatetime_to_dt64,
@@ -1000,6 +1002,42 @@ cdef class _Timestamp(ABCTimestamp):
10001002
# -----------------------------------------------------------------
10011003
# Conversion Methods
10021004

1005+
# TODO: share with _Timedelta?
1006+
@cython.cdivision(False)
1007+
cdef _Timestamp _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=True):
1008+
cdef:
1009+
int64_t value, mult, div, mod
1010+
1011+
if reso == self._reso:
1012+
return self
1013+
1014+
if reso < self._reso:
1015+
# e.g. ns -> us
1016+
mult = get_conversion_factor(reso, self._reso)
1017+
div, mod = divmod(self.value, mult)
1018+
if mod > 0 and not round_ok:
1019+
raise ValueError("Cannot losslessly convert units")
1020+
1021+
# Note that when mod > 0, we follow np.datetime64 in always
1022+
# rounding down.
1023+
value = div
1024+
else:
1025+
mult = get_conversion_factor(self._reso, reso)
1026+
with cython.overflowcheck(True):
1027+
# Note: caller is responsible for re-raising as OutOfBoundsDatetime
1028+
value = self.value * mult
1029+
return type(self)._from_value_and_reso(value, reso=reso, tz=self.tzinfo)
1030+
1031+
def _as_unit(self, str unit, bint round_ok=True):
1032+
dtype = np.dtype(f"M8[{unit}]")
1033+
reso = get_unit_from_dtype(dtype)
1034+
try:
1035+
return self._as_reso(reso, round_ok=round_ok)
1036+
except OverflowError as err:
1037+
raise OutOfBoundsDatetime(
1038+
f"Cannot cast {self} to unit='{unit}' without overflow."
1039+
) from err
1040+
10031041
@property
10041042
def asm8(self) -> np.datetime64:
10051043
"""

pandas/tests/scalar/timedelta/test_timedelta.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
NaT,
1414
iNaT,
1515
)
16+
from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
1617
from pandas.errors import OutOfBoundsTimedelta
1718

1819
import pandas as pd
@@ -33,23 +34,23 @@ def test_as_unit(self):
3334

3435
res = td._as_unit("us")
3536
assert res.value == td.value // 1000
36-
assert res._reso == td._reso - 1
37+
assert res._reso == NpyDatetimeUnit.NPY_FR_us.value
3738

3839
rt = res._as_unit("ns")
3940
assert rt.value == td.value
4041
assert rt._reso == td._reso
4142

4243
res = td._as_unit("ms")
4344
assert res.value == td.value // 1_000_000
44-
assert res._reso == td._reso - 2
45+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
4546

4647
rt = res._as_unit("ns")
4748
assert rt.value == td.value
4849
assert rt._reso == td._reso
4950

5051
res = td._as_unit("s")
5152
assert res.value == td.value // 1_000_000_000
52-
assert res._reso == td._reso - 3
53+
assert res._reso == NpyDatetimeUnit.NPY_FR_s.value
5354

5455
rt = res._as_unit("ns")
5556
assert rt.value == td.value
@@ -58,15 +59,15 @@ def test_as_unit(self):
5859
def test_as_unit_overflows(self):
5960
# microsecond that would be just out of bounds for nano
6061
us = 9223372800000000
61-
td = Timedelta._from_value_and_reso(us, 9)
62+
td = Timedelta._from_value_and_reso(us, NpyDatetimeUnit.NPY_FR_us.value)
6263

6364
msg = "Cannot cast 106752 days 00:00:00 to unit='ns' without overflow"
6465
with pytest.raises(OutOfBoundsTimedelta, match=msg):
6566
td._as_unit("ns")
6667

6768
res = td._as_unit("ms")
6869
assert res.value == us // 1000
69-
assert res._reso == 8
70+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
7071

7172
def test_as_unit_rounding(self):
7273
td = Timedelta(microseconds=1500)
@@ -75,7 +76,7 @@ def test_as_unit_rounding(self):
7576
expected = Timedelta(milliseconds=1)
7677
assert res == expected
7778

78-
assert res._reso == 8
79+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
7980
assert res.value == 1
8081

8182
with pytest.raises(ValueError, match="Cannot losslessly convert units"):

pandas/tests/scalar/timestamp/test_timestamp.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
utc,
1919
)
2020

21+
from pandas._libs.tslibs.dtypes import NpyDatetimeUnit
2122
from pandas._libs.tslibs.timezones import (
2223
dateutil_gettz as gettz,
2324
get_timezone,
2425
)
26+
from pandas.errors import OutOfBoundsDatetime
2527
import pandas.util._test_decorators as td
2628

2729
from pandas import (
@@ -850,3 +852,82 @@ def test_timestamp(self, dt64, ts):
850852
def test_to_period(self, dt64, ts):
851853
alt = Timestamp(dt64)
852854
assert ts.to_period("D") == alt.to_period("D")
855+
856+
857+
class TestAsUnit:
858+
def test_as_unit(self):
859+
ts = Timestamp("1970-01-01")
860+
861+
assert ts._as_unit("ns") is ts
862+
863+
res = ts._as_unit("us")
864+
assert res.value == ts.value // 1000
865+
assert res._reso == NpyDatetimeUnit.NPY_FR_us.value
866+
867+
rt = res._as_unit("ns")
868+
assert rt.value == ts.value
869+
assert rt._reso == ts._reso
870+
871+
res = ts._as_unit("ms")
872+
assert res.value == ts.value // 1_000_000
873+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
874+
875+
rt = res._as_unit("ns")
876+
assert rt.value == ts.value
877+
assert rt._reso == ts._reso
878+
879+
res = ts._as_unit("s")
880+
assert res.value == ts.value // 1_000_000_000
881+
assert res._reso == NpyDatetimeUnit.NPY_FR_s.value
882+
883+
rt = res._as_unit("ns")
884+
assert rt.value == ts.value
885+
assert rt._reso == ts._reso
886+
887+
def test_as_unit_overflows(self):
888+
# microsecond that would be just out of bounds for nano
889+
us = 9223372800000000
890+
ts = Timestamp._from_value_and_reso(us, NpyDatetimeUnit.NPY_FR_us.value, None)
891+
892+
msg = "Cannot cast 2262-04-12 00:00:00 to unit='ns' without overflow"
893+
with pytest.raises(OutOfBoundsDatetime, match=msg):
894+
ts._as_unit("ns")
895+
896+
res = ts._as_unit("ms")
897+
assert res.value == us // 1000
898+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
899+
900+
def test_as_unit_rounding(self):
901+
ts = Timestamp(1_500_000) # i.e. 1500 microseconds
902+
res = ts._as_unit("ms")
903+
904+
expected = Timestamp(1_000_000) # i.e. 1 millisecond
905+
assert res == expected
906+
907+
assert res._reso == NpyDatetimeUnit.NPY_FR_ms.value
908+
assert res.value == 1
909+
910+
with pytest.raises(ValueError, match="Cannot losslessly convert units"):
911+
ts._as_unit("ms", round_ok=False)
912+
913+
def test_as_unit_non_nano(self):
914+
# case where we are going neither to nor from nano
915+
ts = Timestamp("1970-01-02")._as_unit("ms")
916+
assert ts.year == 1970
917+
assert ts.month == 1
918+
assert ts.day == 2
919+
assert ts.hour == ts.minute == ts.second == ts.microsecond == ts.nanosecond == 0
920+
921+
res = ts._as_unit("s")
922+
assert res.value == 24 * 3600
923+
assert res.year == 1970
924+
assert res.month == 1
925+
assert res.day == 2
926+
assert (
927+
res.hour
928+
== res.minute
929+
== res.second
930+
== res.microsecond
931+
== res.nanosecond
932+
== 0
933+
)

0 commit comments

Comments
 (0)