Skip to content

Commit 89b2431

Browse files
s/OverflowError/OutOfBoundsTimedelta where relevant
This is a quick and dirty v0 approach.
1 parent 0957c27 commit 89b2431

File tree

6 files changed

+168
-129
lines changed

6 files changed

+168
-129
lines changed

pandas/_libs/tslibs/timedeltas.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def array_to_timedelta64(
7373
) -> np.ndarray: ... # np.ndarray[m8ns]
7474
def parse_timedelta_unit(unit: str | None) -> UnitChoices: ...
7575
def delta_to_nanoseconds(delta: np.timedelta64 | timedelta | Tick) -> int: ...
76+
def calculate(op, left: int, right: int) -> int: ...
7677

7778
class Timedelta(timedelta):
7879
min: ClassVar[Timedelta]

pandas/_libs/tslibs/timedeltas.pyx

Lines changed: 106 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ cdef dict timedelta_abbrevs = {
136136

137137
_no_input = object()
138138

139+
TIMEDELTA_MIN_NS = np.iinfo(np.int64).min + 1
140+
TIMEDELTA_MAX_NS = np.iinfo(np.int64).max
141+
139142

140143
# ----------------------------------------------------------------------
141144
# API
@@ -217,7 +220,8 @@ cpdef int64_t delta_to_nanoseconds(delta) except? -1:
217220
+ delta.microseconds
218221
) * 1000
219222
except OverflowError as err:
220-
raise OutOfBoundsTimedelta(*err.args) from err
223+
msg = f"{delta} outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
224+
raise OutOfBoundsTimedelta(msg) from err
221225

222226
raise TypeError(type(delta))
223227

@@ -254,7 +258,8 @@ cdef object ensure_td64ns(object ts):
254258
# NB: cython#1381 this cannot be *=
255259
td64_value = td64_value * mult
256260
except OverflowError as err:
257-
raise OutOfBoundsTimedelta(ts) from err
261+
msg = f"{ts} outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
262+
raise OutOfBoundsTimedelta(msg) from err
258263

259264
return np.timedelta64(td64_value, "ns")
260265

@@ -679,6 +684,18 @@ def _op_unary_method(func, name):
679684
return f
680685

681686

687+
cpdef int64_t calculate(object op, int64_t a, int64_t b) except? -1:
688+
"""
689+
Calculate op(a, b) and return the result, or raise if the operation would overflow.
690+
"""
691+
try:
692+
with cython.overflowcheck(True):
693+
return op(a, b)
694+
except OverflowError as ex:
695+
msg = f"outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
696+
raise OutOfBoundsTimedelta(msg) from ex
697+
698+
682699
def _binary_op_method_timedeltalike(op, name):
683700
# define a binary operation that only works if the other argument is
684701
# timedelta like or an array of timedeltalike
@@ -723,13 +740,10 @@ def _binary_op_method_timedeltalike(op, name):
723740
if self._reso != other._reso:
724741
raise NotImplementedError
725742

726-
res = op(self.value, other.value)
727-
if res == NPY_NAT:
728-
# e.g. test_implementation_limits
729-
# TODO: more generally could do an overflowcheck in op?
743+
result = calculate(op, self.value, other.value)
744+
if result == NPY_NAT:
730745
return NaT
731-
732-
return _timedelta_from_value_and_reso(res, reso=self._reso)
746+
return _timedelta_from_value_and_reso(result, self._reso)
733747

734748
f.__name__ = name
735749
return f
@@ -1443,91 +1457,96 @@ class Timedelta(_Timedelta):
14431457
def __new__(cls, object value=_no_input, unit=None, **kwargs):
14441458
cdef _Timedelta td_base
14451459

1446-
if value is _no_input:
1447-
if not len(kwargs):
1448-
raise ValueError("cannot construct a Timedelta without a "
1449-
"value/unit or descriptive keywords "
1450-
"(days,seconds....)")
1460+
try:
1461+
if value is _no_input:
1462+
if not len(kwargs):
1463+
raise ValueError("cannot construct a Timedelta without a "
1464+
"value/unit or descriptive keywords "
1465+
"(days,seconds....)")
1466+
1467+
kwargs = {key: _to_py_int_float(kwargs[key]) for key in kwargs}
1468+
1469+
unsupported_kwargs = set(kwargs)
1470+
unsupported_kwargs.difference_update(cls._req_any_kwargs_new)
1471+
if unsupported_kwargs or not cls._req_any_kwargs_new.intersection(kwargs):
1472+
raise ValueError(
1473+
"cannot construct a Timedelta from the passed arguments, "
1474+
"allowed keywords are "
1475+
"[weeks, days, hours, minutes, seconds, "
1476+
"milliseconds, microseconds, nanoseconds]"
1477+
)
1478+
1479+
# GH43764, convert any input to nanoseconds first and then
1480+
# create the timestamp. This ensures that any potential
1481+
# nanosecond contributions from kwargs parsed as floats
1482+
# are taken into consideration.
1483+
seconds = int((
1484+
(
1485+
(kwargs.get('days', 0) + kwargs.get('weeks', 0) * 7) * 24
1486+
+ kwargs.get('hours', 0)
1487+
) * 3600
1488+
+ kwargs.get('minutes', 0) * 60
1489+
+ kwargs.get('seconds', 0)
1490+
) * 1_000_000_000
1491+
)
14511492

1452-
kwargs = {key: _to_py_int_float(kwargs[key]) for key in kwargs}
1493+
value = np.timedelta64(
1494+
int(kwargs.get('nanoseconds', 0))
1495+
+ int(kwargs.get('microseconds', 0) * 1_000)
1496+
+ int(kwargs.get('milliseconds', 0) * 1_000_000)
1497+
+ seconds
1498+
)
14531499

1454-
unsupported_kwargs = set(kwargs)
1455-
unsupported_kwargs.difference_update(cls._req_any_kwargs_new)
1456-
if unsupported_kwargs or not cls._req_any_kwargs_new.intersection(kwargs):
1500+
if unit in {'Y', 'y', 'M'}:
14571501
raise ValueError(
1458-
"cannot construct a Timedelta from the passed arguments, "
1459-
"allowed keywords are "
1460-
"[weeks, days, hours, minutes, seconds, "
1461-
"milliseconds, microseconds, nanoseconds]"
1502+
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1503+
"represent unambiguous timedelta values durations."
14621504
)
14631505

1464-
# GH43764, convert any input to nanoseconds first and then
1465-
# create the timestamp. This ensures that any potential
1466-
# nanosecond contributions from kwargs parsed as floats
1467-
# are taken into consideration.
1468-
seconds = int((
1469-
(
1470-
(kwargs.get('days', 0) + kwargs.get('weeks', 0) * 7) * 24
1471-
+ kwargs.get('hours', 0)
1472-
) * 3600
1473-
+ kwargs.get('minutes', 0) * 60
1474-
+ kwargs.get('seconds', 0)
1475-
) * 1_000_000_000
1476-
)
1477-
1478-
value = np.timedelta64(
1479-
int(kwargs.get('nanoseconds', 0))
1480-
+ int(kwargs.get('microseconds', 0) * 1_000)
1481-
+ int(kwargs.get('milliseconds', 0) * 1_000_000)
1482-
+ seconds
1483-
)
1484-
1485-
if unit in {'Y', 'y', 'M'}:
1486-
raise ValueError(
1487-
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1488-
"represent unambiguous timedelta values durations."
1489-
)
1490-
1491-
# GH 30543 if pd.Timedelta already passed, return it
1492-
# check that only value is passed
1493-
if isinstance(value, _Timedelta) and unit is None and len(kwargs) == 0:
1494-
return value
1495-
elif isinstance(value, _Timedelta):
1496-
value = value.value
1497-
elif isinstance(value, str):
1498-
if unit is not None:
1499-
raise ValueError("unit must not be specified if the value is a str")
1500-
if (len(value) > 0 and value[0] == 'P') or (
1501-
len(value) > 1 and value[:2] == '-P'
1502-
):
1503-
value = parse_iso_format_string(value)
1506+
# GH 30543 if pd.Timedelta already passed, return it
1507+
# check that only value is passed
1508+
if isinstance(value, _Timedelta) and unit is None and len(kwargs) == 0:
1509+
return value
1510+
elif isinstance(value, _Timedelta):
1511+
value = value.value
1512+
elif isinstance(value, str):
1513+
if unit is not None:
1514+
raise ValueError("unit must not be specified if the value is a str")
1515+
if (len(value) > 0 and value[0] == 'P') or (
1516+
len(value) > 1 and value[:2] == '-P'
1517+
):
1518+
value = parse_iso_format_string(value)
1519+
else:
1520+
value = parse_timedelta_string(value)
1521+
value = np.timedelta64(value)
1522+
elif PyDelta_Check(value):
1523+
value = convert_to_timedelta64(value, 'ns')
1524+
elif is_timedelta64_object(value):
1525+
value = ensure_td64ns(value)
1526+
elif is_tick_object(value):
1527+
value = np.timedelta64(value.nanos, 'ns')
1528+
elif is_integer_object(value) or is_float_object(value):
1529+
# unit=None is de-facto 'ns'
1530+
unit = parse_timedelta_unit(unit)
1531+
value = convert_to_timedelta64(value, unit)
1532+
elif checknull_with_nat(value):
1533+
return NaT
15041534
else:
1505-
value = parse_timedelta_string(value)
1506-
value = np.timedelta64(value)
1507-
elif PyDelta_Check(value):
1508-
value = convert_to_timedelta64(value, 'ns')
1509-
elif is_timedelta64_object(value):
1510-
value = ensure_td64ns(value)
1511-
elif is_tick_object(value):
1512-
value = np.timedelta64(value.nanos, 'ns')
1513-
elif is_integer_object(value) or is_float_object(value):
1514-
# unit=None is de-facto 'ns'
1515-
unit = parse_timedelta_unit(unit)
1516-
value = convert_to_timedelta64(value, unit)
1517-
elif checknull_with_nat(value):
1518-
return NaT
1519-
else:
1520-
raise ValueError(
1521-
"Value must be Timedelta, string, integer, "
1522-
f"float, timedelta or convertible, not {type(value).__name__}"
1523-
)
1535+
raise ValueError(
1536+
"Value must be Timedelta, string, integer, "
1537+
f"float, timedelta or convertible, not {type(value).__name__}"
1538+
)
15241539

1525-
if is_timedelta64_object(value):
1526-
value = value.view('i8')
1540+
if is_timedelta64_object(value):
1541+
value = value.view('i8')
15271542

1528-
# nat
1529-
if value == NPY_NAT:
1530-
return NaT
1543+
# nat
1544+
if value == NPY_NAT:
1545+
return NaT
1546+
1547+
except OverflowError as ex:
1548+
msg = f"outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
1549+
raise OutOfBoundsTimedelta(msg) from ex
15311550

15321551
return _timedelta_from_value_and_reso(value, NPY_FR_ns)
15331552

@@ -1824,6 +1843,6 @@ cdef _broadcast_floordiv_td64(
18241843

18251844

18261845
# resolution in ns
1827-
Timedelta.min = Timedelta(np.iinfo(np.int64).min + 1)
1828-
Timedelta.max = Timedelta(np.iinfo(np.int64).max)
1846+
Timedelta.min = Timedelta(TIMEDELTA_MIN_NS)
1847+
Timedelta.max = Timedelta(TIMEDELTA_MAX_NS)
18291848
Timedelta.resolution = Timedelta(nanoseconds=1)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from __future__ import annotations
2+
3+
import re
4+
5+
import pytest
6+
7+
from pandas._libs.tslibs import OutOfBoundsTimedelta
8+
9+
10+
@pytest.fixture()
11+
def timedelta_overflow() -> dict:
12+
"""
13+
The expected message and exception when Timedelta ops overflow.
14+
"""
15+
msg = re.escape(
16+
"outside allowed range [-9223372036854775807ns, 9223372036854775807ns]"
17+
)
18+
return {"expected_exception": OutOfBoundsTimedelta, "match": msg}

pandas/tests/scalar/timedelta/test_arithmetic.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@
1010
import numpy as np
1111
import pytest
1212

13-
from pandas.errors import OutOfBoundsTimedelta
14-
1513
import pandas as pd
1614
from pandas import (
1715
NaT,
@@ -98,12 +96,11 @@ def test_td_add_datetimelike_scalar(self, op):
9896
result = op(td, NaT)
9997
assert result is NaT
10098

101-
def test_td_add_timestamp_overflow(self):
102-
msg = "int too (large|big) to convert"
103-
with pytest.raises(OverflowError, match=msg):
99+
def test_td_add_timestamp_overflow(self, timedelta_overflow):
100+
with pytest.raises(**timedelta_overflow):
104101
Timestamp("1700-01-01") + Timedelta(13 * 19999, unit="D")
105102

106-
with pytest.raises(OutOfBoundsTimedelta, match=msg):
103+
with pytest.raises(**timedelta_overflow):
107104
Timestamp("1700-01-01") + timedelta(days=13 * 19999)
108105

109106
@pytest.mark.parametrize("op", [operator.add, ops.radd])

0 commit comments

Comments
 (0)