Skip to content

Commit caf261f

Browse files
authored
REF: re-use convert_reso (#47807)
* REF: re-use convert_reso * typo fixup
1 parent 187636f commit caf261f

File tree

5 files changed

+91
-55
lines changed

5 files changed

+91
-55
lines changed

pandas/_libs/tslibs/np_datetime.pxd

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,10 @@ cdef bint cmp_dtstructs(npy_datetimestruct* left, npy_datetimestruct* right, int
109109
cdef get_implementation_bounds(
110110
NPY_DATETIMEUNIT reso, npy_datetimestruct *lower, npy_datetimestruct *upper
111111
)
112+
113+
cdef int64_t convert_reso(
114+
int64_t value,
115+
NPY_DATETIMEUNIT from_reso,
116+
NPY_DATETIMEUNIT to_reso,
117+
bint round_ok,
118+
) except? -1

pandas/_libs/tslibs/np_datetime.pyx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -541,5 +541,59 @@ cdef int64_t get_conversion_factor(NPY_DATETIMEUNIT from_unit, NPY_DATETIMEUNIT
541541
return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_fs, to_unit)
542542
elif from_unit == NPY_DATETIMEUNIT.NPY_FR_fs:
543543
return 1000 * get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_as, to_unit)
544+
545+
546+
cdef int64_t convert_reso(
547+
int64_t value,
548+
NPY_DATETIMEUNIT from_reso,
549+
NPY_DATETIMEUNIT to_reso,
550+
bint round_ok,
551+
) except? -1:
552+
cdef:
553+
int64_t res_value, mult, div, mod
554+
555+
if from_reso == to_reso:
556+
return value
557+
558+
elif to_reso < from_reso:
559+
# e.g. ns -> us, no risk of overflow, but can be lossy rounding
560+
mult = get_conversion_factor(to_reso, from_reso)
561+
div, mod = divmod(value, mult)
562+
if mod > 0 and not round_ok:
563+
raise ValueError("Cannot losslessly convert units")
564+
565+
# Note that when mod > 0, we follow np.timedelta64 in always
566+
# rounding down.
567+
res_value = div
568+
569+
elif (
570+
from_reso == NPY_FR_Y
571+
or from_reso == NPY_FR_M
572+
or to_reso == NPY_FR_Y
573+
or to_reso == NPY_FR_M
574+
):
575+
# Converting by multiplying isn't _quite_ right bc the number of
576+
# seconds in a month/year isn't fixed.
577+
res_value = _convert_reso_with_dtstruct(value, from_reso, to_reso)
578+
544579
else:
545-
raise ValueError(from_unit, to_unit)
580+
# e.g. ns -> us, risk of overflow, but no risk of lossy rounding
581+
mult = get_conversion_factor(from_reso, to_reso)
582+
with cython.overflowcheck(True):
583+
# Note: caller is responsible for re-raising as OutOfBoundsTimedelta
584+
res_value = value * mult
585+
586+
return res_value
587+
588+
589+
cdef int64_t _convert_reso_with_dtstruct(
590+
int64_t value,
591+
NPY_DATETIMEUNIT from_unit,
592+
NPY_DATETIMEUNIT to_unit,
593+
) except? -1:
594+
cdef:
595+
npy_datetimestruct dts
596+
597+
pandas_datetime_to_datetimestruct(value, from_unit, &dts)
598+
check_dts_bounds(&dts, to_unit)
599+
return npy_datetimestruct_to_datetime(to_unit, &dts)

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ from pandas._libs.tslibs.np_datetime cimport (
4747
NPY_FR_ns,
4848
cmp_dtstructs,
4949
cmp_scalar,
50+
convert_reso,
5051
get_conversion_factor,
5152
get_datetime64_unit,
5253
get_timedelta64_value,
@@ -57,7 +58,10 @@ from pandas._libs.tslibs.np_datetime cimport (
5758
pandas_timedeltastruct,
5859
)
5960

60-
from pandas._libs.tslibs.np_datetime import OutOfBoundsTimedelta
61+
from pandas._libs.tslibs.np_datetime import (
62+
OutOfBoundsDatetime,
63+
OutOfBoundsTimedelta,
64+
)
6165

6266
from pandas._libs.tslibs.offsets cimport is_tick_object
6367
from pandas._libs.tslibs.util cimport (
@@ -240,6 +244,11 @@ cpdef int64_t delta_to_nanoseconds(
240244

241245
elif is_timedelta64_object(delta):
242246
in_reso = get_datetime64_unit(delta)
247+
if in_reso == NPY_DATETIMEUNIT.NPY_FR_Y or in_reso == NPY_DATETIMEUNIT.NPY_FR_M:
248+
raise ValueError(
249+
"delta_to_nanoseconds does not support Y or M units, "
250+
"as their duration in nanoseconds is ambiguous."
251+
)
243252
n = get_timedelta64_value(delta)
244253

245254
elif PyDelta_Check(delta):
@@ -256,26 +265,15 @@ cpdef int64_t delta_to_nanoseconds(
256265
else:
257266
raise TypeError(type(delta))
258267

259-
if reso < in_reso:
260-
# e.g. ns -> us
261-
factor = get_conversion_factor(reso, in_reso)
262-
div, mod = divmod(n, factor)
263-
if mod > 0 and not round_ok:
264-
raise ValueError("Cannot losslessly convert units")
265-
266-
# Note that when mod > 0, we follow np.timedelta64 in always
267-
# rounding down.
268-
value = div
269-
else:
270-
factor = get_conversion_factor(in_reso, reso)
271-
try:
272-
with cython.overflowcheck(True):
273-
value = n * factor
274-
except OverflowError as err:
275-
unit_str = npy_unit_to_abbrev(reso)
276-
raise OutOfBoundsTimedelta(
277-
f"Cannot cast {str(delta)} to unit={unit_str} without overflow."
278-
) from err
268+
try:
269+
return convert_reso(n, in_reso, reso, round_ok=round_ok)
270+
except (OutOfBoundsDatetime, OverflowError) as err:
271+
# Catch OutOfBoundsDatetime bc convert_reso can call check_dts_bounds
272+
# for Y/M-resolution cases
273+
unit_str = npy_unit_to_abbrev(reso)
274+
raise OutOfBoundsTimedelta(
275+
f"Cannot cast {str(delta)} to unit={unit_str} without overflow."
276+
) from err
279277

280278
return value
281279

@@ -1538,21 +1536,7 @@ cdef class _Timedelta(timedelta):
15381536
if reso == self._reso:
15391537
return self
15401538

1541-
if reso < self._reso:
1542-
# e.g. ns -> us
1543-
mult = get_conversion_factor(reso, self._reso)
1544-
div, mod = divmod(self.value, mult)
1545-
if mod > 0 and not round_ok:
1546-
raise ValueError("Cannot losslessly convert units")
1547-
1548-
# Note that when mod > 0, we follow np.timedelta64 in always
1549-
# rounding down.
1550-
value = div
1551-
else:
1552-
mult = get_conversion_factor(self._reso, reso)
1553-
with cython.overflowcheck(True):
1554-
# Note: caller is responsible for re-raising as OutOfBoundsTimedelta
1555-
value = self.value * mult
1539+
value = convert_reso(self.value, self._reso, reso, round_ok=round_ok)
15561540
return type(self)._from_value_and_reso(value, reso=reso)
15571541

15581542

pandas/_libs/tslibs/timestamps.pyx

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ from pandas._libs.tslibs.np_datetime cimport (
8282
NPY_FR_ns,
8383
cmp_dtstructs,
8484
cmp_scalar,
85+
convert_reso,
8586
get_conversion_factor,
8687
get_datetime64_unit,
8788
get_datetime64_value,
@@ -1043,7 +1044,6 @@ cdef class _Timestamp(ABCTimestamp):
10431044
# -----------------------------------------------------------------
10441045
# Conversion Methods
10451046

1046-
# TODO: share with _Timedelta?
10471047
@cython.cdivision(False)
10481048
cdef _Timestamp _as_reso(self, NPY_DATETIMEUNIT reso, bint round_ok=True):
10491049
cdef:
@@ -1052,21 +1052,7 @@ cdef class _Timestamp(ABCTimestamp):
10521052
if reso == self._reso:
10531053
return self
10541054

1055-
if reso < self._reso:
1056-
# e.g. ns -> us
1057-
mult = get_conversion_factor(reso, self._reso)
1058-
div, mod = divmod(self.value, mult)
1059-
if mod > 0 and not round_ok:
1060-
raise ValueError("Cannot losslessly convert units")
1061-
1062-
# Note that when mod > 0, we follow np.datetime64 in always
1063-
# rounding down.
1064-
value = div
1065-
else:
1066-
mult = get_conversion_factor(self._reso, reso)
1067-
with cython.overflowcheck(True):
1068-
# Note: caller is responsible for re-raising as OutOfBoundsDatetime
1069-
value = self.value * mult
1055+
value = convert_reso(self.value, self._reso, reso, round_ok=round_ok)
10701056
return type(self)._from_value_and_reso(value, reso=reso, tz=self.tzinfo)
10711057

10721058
def _as_unit(self, str unit, bint round_ok=True):

pandas/tests/tslibs/test_timedeltas.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,19 @@ def test_delta_to_nanoseconds_error():
5656

5757

5858
def test_delta_to_nanoseconds_td64_MY_raises():
59+
msg = (
60+
"delta_to_nanoseconds does not support Y or M units, "
61+
"as their duration in nanoseconds is ambiguous"
62+
)
63+
5964
td = np.timedelta64(1234, "Y")
6065

61-
with pytest.raises(ValueError, match="0, 10"):
66+
with pytest.raises(ValueError, match=msg):
6267
delta_to_nanoseconds(td)
6368

6469
td = np.timedelta64(1234, "M")
6570

66-
with pytest.raises(ValueError, match="1, 10"):
71+
with pytest.raises(ValueError, match=msg):
6772
delta_to_nanoseconds(td)
6873

6974

0 commit comments

Comments
 (0)