Skip to content

Commit c9e209e

Browse files
consolidate Timedelta creation
1 parent 36eb26f commit c9e209e

File tree

2 files changed

+88
-99
lines changed

2 files changed

+88
-99
lines changed

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 83 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import_datetime()
3232

3333
cimport pandas._libs.tslibs.util as util
3434
from pandas._libs cimport ops
35+
from pandas._libs.missing cimport C_NA
3536
from pandas._libs.tslibs.base cimport ABCTimestamp
3637
from pandas._libs.tslibs.conversion cimport (
3738
cast_from_unit,
@@ -308,21 +309,6 @@ cdef convert_to_timedelta64(object ts, str unit):
308309
return ts.astype("timedelta64[ns]")
309310

310311

311-
cpdef to_timedelta64(object value, str unit):
312-
"""
313-
Wrapper around convert_to_timedelta64() that does overflow checks.
314-
TODO: also construct non-nano
315-
TODO: do all overflow-unsafe operations here
316-
TODO: constrain unit to a more specific type
317-
"""
318-
with cython.overflowcheck(True):
319-
try:
320-
return convert_to_timedelta64(value, unit)
321-
except OverflowError as ex:
322-
msg = f"{value} outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
323-
raise OutOfBoundsTimedelta(msg) from ex
324-
325-
326312
@cython.boundscheck(False)
327313
@cython.wraparound(False)
328314
def array_to_timedelta64(
@@ -682,8 +668,7 @@ cdef bint _validate_ops_compat(other):
682668

683669
def _op_unary_method(func, name):
684670
def f(self):
685-
new_value = func(self.value)
686-
return _timedelta_from_value_and_reso(new_value, self._reso)
671+
return create_timedelta(func(self.value), "ignore", self._reso)
687672
f.__name__ = name
688673
return f
689674

@@ -700,20 +685,6 @@ cpdef int64_t calc_int_int(object op, object a, object b) except? -1:
700685
raise OutOfBoundsTimedelta(msg) from ex
701686

702687

703-
cpdef int64_t calc_int_float(object op, object a, object b) except? -1:
704-
"""
705-
Calculate op(int, double), raising if any of the following aren't safe conversions:
706-
- a to int64_t
707-
- b to double
708-
- result to int64_t
709-
"""
710-
try:
711-
return ops.calc_int_float(op, a, b)
712-
except OverflowError as ex:
713-
msg = f"outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
714-
raise OutOfBoundsTimedelta(msg) from ex
715-
716-
717688
def _binary_op_method_timedeltalike(op, name):
718689
# define a binary operation that only works if the other argument is
719690
# timedelta like or an array of timedeltalike
@@ -758,10 +729,7 @@ def _binary_op_method_timedeltalike(op, name):
758729
if self._reso != other._reso:
759730
raise NotImplementedError
760731

761-
result = calc_int_int(op, self.value, other.value)
762-
if result == NPY_NAT:
763-
return NaT
764-
return _timedelta_from_value_and_reso(result, self._reso)
732+
return create_timedelta(op(self.value, other.value), "ignore", self._reso)
765733

766734
f.__name__ = name
767735
return f
@@ -892,7 +860,7 @@ cdef _to_py_int_float(v):
892860

893861

894862
def _timedelta_unpickle(value, reso):
895-
return _timedelta_from_value_and_reso(value, reso)
863+
return create_timedelta(value, "ignore", reso)
896864

897865

898866
cdef _timedelta_from_value_and_reso(int64_t value, NPY_DATETIMEUNIT reso):
@@ -923,6 +891,44 @@ cdef _timedelta_from_value_and_reso(int64_t value, NPY_DATETIMEUNIT reso):
923891
return td_base
924892

925893

894+
@cython.overflowcheck(True)
895+
cdef object create_timedelta(object value, str in_unit, NPY_DATETIMEUNIT out_reso):
896+
"""
897+
Timedelta factory.
898+
899+
Timedelta.__new__ just does arg validation (at least currently). Also, some internal
900+
functions expect to be able to create non-nano reso Timedeltas, but Timedelta.__new__
901+
doesn't yet expose that.
902+
903+
_timedelta_from_value_and_reso does, but only accepts limited args, and doesn't check for overflow.
904+
"""
905+
cdef:
906+
int64_t out_value
907+
908+
if isinstance(value, _Timedelta):
909+
return value
910+
if value is C_NA:
911+
raise ValueError("Not supported")
912+
913+
try:
914+
# if unit == "ns", no need to create an m8[ns] just to read the (same) value back
915+
# if unit == "ignore", assume caller wants to invoke an overflow-safe version of
916+
# _timedelta_from_value_and_reso, and that any float rounding is acceptable
917+
if (is_integer_object(value) or is_float_object(value)) and in_unit in ("ns", "ignore"):
918+
if util.is_nan(value):
919+
return NaT
920+
out_value = <int64_t>value
921+
else:
922+
out_value = convert_to_timedelta64(value, in_unit).view(np.int64)
923+
except OverflowError as ex:
924+
msg = f"{value} outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
925+
raise OutOfBoundsTimedelta(msg) from ex
926+
927+
if out_value == NPY_NAT:
928+
return NaT
929+
return _timedelta_from_value_and_reso(out_value, out_reso)
930+
931+
926932
# Similar to Timestamp/datetime, this is a construction requirement for
927933
# timedeltas that we need to do object instantiation in python. This will
928934
# serve as a C extension type that shadows the Python class, where we do any
@@ -1406,7 +1412,7 @@ cdef class _Timedelta(timedelta):
14061412
@classmethod
14071413
def _from_value_and_reso(cls, int64_t value, NPY_DATETIMEUNIT reso):
14081414
# exposing as classmethod for testing
1409-
return _timedelta_from_value_and_reso(value, reso)
1415+
return create_timedelta(value, "ignore", reso)
14101416

14111417

14121418
# Python front end to C extension type _Timedelta
@@ -1474,37 +1480,27 @@ class Timedelta(_Timedelta):
14741480
)
14751481

14761482
def __new__(cls, object value=_no_input, unit=None, **kwargs):
1477-
cdef _Timedelta td_base
1478-
1479-
if isinstance(value, _Timedelta):
1480-
return value
1481-
if checknull_with_nat(value):
1482-
return NaT
1483-
1484-
if unit in {"Y", "y", "M"}:
1485-
raise ValueError(
1486-
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1487-
"represent unambiguous timedelta values durations."
1488-
)
1489-
if isinstance(value, str) and unit is not None:
1490-
raise ValueError("unit must not be specified if the value is a str")
1491-
elif value is _no_input and not kwargs:
1492-
raise ValueError(
1493-
"cannot construct a Timedelta without a value/unit "
1494-
"or descriptive keywords (days,seconds....)"
1495-
)
1496-
if not kwargs.keys() <= set(cls._allowed_kwargs):
1497-
raise ValueError(
1498-
"cannot construct a Timedelta from the passed arguments, "
1499-
f"allowed keywords are {cls._allowed_kwargs}"
1500-
)
1483+
cdef:
1484+
_Timedelta td_base
1485+
NPY_DATETIMEUNIT out_reso = NPY_FR_ns
15011486

1502-
# GH43764, convert any input to nanoseconds first, to ensure any potential
1503-
# nanosecond contributions from kwargs parsed as floats are included
1504-
kwargs = collections.defaultdict(int, {key: _to_py_int_float(val) for key, val in kwargs.items()})
1505-
if kwargs:
1506-
value = to_timedelta64(
1507-
sum((
1487+
# process kwargs iff no value passed
1488+
if value is _no_input:
1489+
if not kwargs:
1490+
raise ValueError(
1491+
"cannot construct a Timedelta without a value/unit "
1492+
"or descriptive keywords (days,seconds....)"
1493+
)
1494+
if not kwargs.keys() <= set(cls._allowed_kwargs):
1495+
raise ValueError(
1496+
"cannot construct a Timedelta from the passed arguments, "
1497+
f"allowed keywords are {cls._allowed_kwargs}"
1498+
)
1499+
# GH43764, convert any input to nanoseconds first, to ensure any potential
1500+
# nanosecond contributions from kwargs parsed as floats are included
1501+
kwargs = collections.defaultdict(int, {key: _to_py_int_float(val) for key, val in kwargs.items()})
1502+
ns = sum(
1503+
(
15081504
kwargs["weeks"] * 7 * 24 * 3600 * 1_000_000_000,
15091505
kwargs["days"] * 24 * 3600 * 1_000_000_000,
15101506
kwargs["hours"] * 3600 * 1_000_000_000,
@@ -1513,19 +1509,18 @@ class Timedelta(_Timedelta):
15131509
kwargs["milliseconds"] * 1_000_000,
15141510
kwargs["microseconds"] * 1_000,
15151511
kwargs["nanoseconds"],
1516-
)),
1517-
"ns",
1512+
)
15181513
)
1519-
else:
1520-
if is_integer_object(value) or is_float_object(value):
1521-
unit = parse_timedelta_unit(unit)
1522-
else:
1523-
unit = "ns"
1524-
value = to_timedelta64(value, unit)
1514+
return create_timedelta(ns, "ns", out_reso)
15251515

1526-
if is_td64nat(value):
1527-
return NaT
1528-
return _timedelta_from_value_and_reso(value.view("i8"), NPY_FR_ns)
1516+
if isinstance(value, str) and unit is not None:
1517+
raise ValueError("unit must not be specified if the value is a str")
1518+
elif unit in {"Y", "y", "M"}:
1519+
raise ValueError(
1520+
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1521+
"represent unambiguous timedelta values durations."
1522+
)
1523+
return create_timedelta(value, parse_timedelta_unit(unit), out_reso)
15291524

15301525
def __setstate__(self, state):
15311526
if len(state) == 1:
@@ -1602,14 +1597,14 @@ class Timedelta(_Timedelta):
16021597
# Arithmetic Methods
16031598
# TODO: Can some of these be defined in the cython class?
16041599

1605-
__neg__ = _op_unary_method(lambda x: -x, '__neg__')
1606-
__pos__ = _op_unary_method(lambda x: x, '__pos__')
1607-
__abs__ = _op_unary_method(lambda x: abs(x), '__abs__')
1600+
__neg__ = _op_unary_method(operator.neg, "__neg__")
1601+
__pos__ = _op_unary_method(operator.pos, "__pos__")
1602+
__abs__ = _op_unary_method(operator.abs, "__abs__")
16081603

1609-
__add__ = _binary_op_method_timedeltalike(lambda x, y: x + y, '__add__')
1610-
__radd__ = _binary_op_method_timedeltalike(lambda x, y: x + y, '__radd__')
1611-
__sub__ = _binary_op_method_timedeltalike(lambda x, y: x - y, '__sub__')
1612-
__rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, '__rsub__')
1604+
__add__ = _binary_op_method_timedeltalike(operator.add, "__add__")
1605+
__radd__ = _binary_op_method_timedeltalike(operator.add, "__radd__")
1606+
__sub__ = _binary_op_method_timedeltalike(operator.sub, "__sub__")
1607+
__rsub__ = _binary_op_method_timedeltalike(lambda x, y: y - x, "__rsub__")
16131608

16141609
def __mul__(self, other):
16151610
if util.is_nan(other):
@@ -1618,13 +1613,9 @@ class Timedelta(_Timedelta):
16181613
if is_array(other):
16191614
# ndarray-like
16201615
return other * self.to_timedelta64()
1621-
if is_integer_object(other):
1622-
value = calc_int_int(operator.mul, self.value, other)
1623-
return _timedelta_from_value_and_reso(value, self._reso)
1624-
if is_float_object(other):
1625-
value = calc_int_float(operator.mul, self.value, other)
1626-
return _timedelta_from_value_and_reso(value, self._reso)
1627-
1616+
if is_integer_object(other) or is_float_object(other):
1617+
# can't call Timedelta b/c it doesn't (yet) expose reso
1618+
return create_timedelta(self.value * other, "ignore", self._reso)
16281619
return NotImplemented
16291620

16301621
__rmul__ = __mul__

pandas/tests/scalar/timedelta/test_timedelta.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def test_from_offset(self, tick_classes):
339339
offset = tick_classes(1)
340340
assert Timedelta(offset).value == offset.nanos
341341

342-
@pytest.mark.parametrize("td_unit", TD_UNITS)
342+
@pytest.mark.parametrize("td_unit", chain.from_iterable(TD_UNITS))
343343
def test_from_td64_ignores_unit(self, td_unit: str, td_overflow_msg: str):
344344
"""
345345
Ignore the unit, as it may cause silently overflows leading to incorrect
@@ -355,7 +355,6 @@ def test_from_td64_ignores_unit(self, td_unit: str, td_overflow_msg: str):
355355
("args", "kwargs"),
356356
[
357357
((), {}),
358-
(("ps",), {}),
359358
(("ns",), {}),
360359
(("ms",), {}),
361360
((), {"seconds": 3}),
@@ -367,8 +366,6 @@ def test_from_td_ignores_other_args(self, args: tuple, kwargs: dict):
367366
new = Timedelta(original, *args, **kwargs)
368367

369368
assert new == original
370-
if not any((args, kwargs)):
371-
assert new is original
372369

373370
def test_from_timedelta(self, timedelta_kwarg: str):
374371
kwargs = {timedelta_kwarg: 1}
@@ -601,11 +598,12 @@ def test_sub_preserves_reso(self, non_nano_td, non_nano_reso):
601598
assert res == expected
602599
assert res._reso == non_nano_reso
603600

604-
def test_mul_preserves_reso(self, non_nano_td, non_nano_reso):
601+
@pytest.mark.parametrize("factor", (2, 2.5))
602+
def test_mul_preserves_reso(self, non_nano_td, non_nano_reso, factor):
605603
# The non_nano_td fixture should always be far from the implementation
606604
# bound, so doubling does not risk overflow.
607-
res = non_nano_td * 2
608-
assert res.value == non_nano_td.value * 2
605+
res = non_nano_td * factor
606+
assert res.value == non_nano_td.value * factor
609607
assert res._reso == non_nano_reso
610608

611609
def test_cmp_cross_reso(self, non_nano_td):

0 commit comments

Comments
 (0)