Skip to content

Commit d53d1ce

Browse files
simplify Timedelta ctor, fix tests
1 parent ef09bb9 commit d53d1ce

File tree

4 files changed

+53
-95
lines changed

4 files changed

+53
-95
lines changed

pandas/_libs/tslibs/timedeltas.pyx

+49-84
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ from pandas._libs.tslibs.nattype cimport (
4242
c_NaT as NaT,
4343
c_nat_strings as nat_strings,
4444
checknull_with_nat,
45+
is_td64nat,
4546
)
4647
from pandas._libs.tslibs.np_datetime cimport (
4748
NPY_DATETIMEUNIT,
@@ -1452,104 +1453,68 @@ class Timedelta(_Timedelta):
14521453
We see that either way we get the same result
14531454
"""
14541455

1455-
_req_any_kwargs_new = {"weeks", "days", "hours", "minutes", "seconds",
1456-
"milliseconds", "microseconds", "nanoseconds"}
1456+
_allowed_kwargs = (
1457+
"weeks", "days", "hours", "minutes", "seconds", "milliseconds", "microseconds", "nanoseconds"
1458+
)
14571459

14581460
def __new__(cls, object value=_no_input, unit=None, **kwargs):
14591461
cdef _Timedelta td_base
14601462

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

1494-
value = np.timedelta64(
1495-
int(kwargs.get('nanoseconds', 0))
1496-
+ int(kwargs.get('microseconds', 0) * 1_000)
1497-
+ int(kwargs.get('milliseconds', 0) * 1_000_000)
1498-
+ seconds
1468+
if unit in {"Y", "y", "M"}:
1469+
raise ValueError(
1470+
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1471+
"represent unambiguous timedelta values durations."
1472+
)
1473+
if isinstance(value, str) and unit is not None:
1474+
raise ValueError("unit must not be specified if the value is a str")
1475+
elif value is _no_input:
1476+
if not kwargs:
1477+
raise ValueError(
1478+
"cannot construct a Timedelta without a value/unit "
1479+
"or descriptive keywords (days,seconds....)"
14991480
)
1500-
1501-
if unit in {'Y', 'y', 'M'}:
1481+
if not kwargs.keys() <= set(cls._allowed_kwargs):
15021482
raise ValueError(
1503-
"Units 'M', 'Y', and 'y' are no longer supported, as they do not "
1504-
"represent unambiguous timedelta values durations."
1483+
"cannot construct a Timedelta from the passed arguments, "
1484+
f"allowed keywords are {cls._allowed_kwargs}"
15051485
)
15061486

1507-
# GH 30543 if pd.Timedelta already passed, return it
1508-
# check that only value is passed
1509-
if isinstance(value, _Timedelta) and unit is None and len(kwargs) == 0:
1510-
return value
1511-
elif isinstance(value, _Timedelta):
1512-
value = value.value
1513-
elif isinstance(value, str):
1514-
if unit is not None:
1515-
raise ValueError("unit must not be specified if the value is a str")
1516-
if (len(value) > 0 and value[0] == 'P') or (
1517-
len(value) > 1 and value[:2] == '-P'
1518-
):
1519-
value = parse_iso_format_string(value)
1487+
try:
1488+
# GH43764, convert any input to nanoseconds first, to ensure any potential
1489+
# nanosecond contributions from kwargs parsed as floats are included
1490+
kwargs = collections.defaultdict(int, {key: _to_py_int_float(val) for key, val in kwargs.items()})
1491+
if kwargs:
1492+
value = convert_to_timedelta64(
1493+
sum((
1494+
kwargs["weeks"] * 7 * 24 * 3600 * 1_000_000_000,
1495+
kwargs["days"] * 24 * 3600 * 1_000_000_000,
1496+
kwargs["hours"] * 3600 * 1_000_000_000,
1497+
kwargs["minutes"] * 60 * 1_000_000_000,
1498+
kwargs["seconds"] * 1_000_000_000,
1499+
kwargs["milliseconds"] * 1_000_000,
1500+
kwargs["microseconds"] * 1_000,
1501+
kwargs["nanoseconds"],
1502+
)),
1503+
"ns",
1504+
)
1505+
else:
1506+
if is_integer_object(value) or is_float_object(value):
1507+
unit = parse_timedelta_unit(unit)
15201508
else:
1521-
value = parse_timedelta_string(value)
1522-
value = np.timedelta64(value)
1523-
elif PyDelta_Check(value):
1524-
value = convert_to_timedelta64(value, 'ns')
1525-
elif is_timedelta64_object(value):
1526-
value = ensure_td64ns(value)
1527-
elif is_tick_object(value):
1528-
value = np.timedelta64(value.nanos, 'ns')
1529-
elif is_integer_object(value) or is_float_object(value):
1530-
# unit=None is de-facto 'ns'
1531-
unit = parse_timedelta_unit(unit)
1509+
unit = "ns"
15321510
value = convert_to_timedelta64(value, unit)
1533-
elif checknull_with_nat(value):
1534-
return NaT
1535-
else:
1536-
raise ValueError(
1537-
"Value must be Timedelta, string, integer, "
1538-
f"float, timedelta or convertible, not {type(value).__name__}"
1539-
)
1540-
1541-
if is_timedelta64_object(value):
1542-
value = value.view('i8')
1543-
1544-
# nat
1545-
if value == NPY_NAT:
1546-
return NaT
1547-
15481511
except OverflowError as ex:
15491512
msg = f"outside allowed range [{TIMEDELTA_MIN_NS}ns, {TIMEDELTA_MAX_NS}ns]"
15501513
raise OutOfBoundsTimedelta(msg) from ex
1551-
1552-
return _timedelta_from_value_and_reso(value, NPY_FR_ns)
1514+
else:
1515+
if is_td64nat(value):
1516+
return NaT
1517+
return _timedelta_from_value_and_reso(value.view("i8"), NPY_FR_ns)
15531518

15541519
def __setstate__(self, state):
15551520
if len(state) == 1:

pandas/tests/scalar/timedelta/test_constructors.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,8 @@ def test_raises_if_no_args_passed(self):
343343
def test_raises_for_invalid_kwarg(self, unit: str):
344344
msg = re.escape(
345345
"cannot construct a Timedelta from the passed arguments, allowed keywords "
346-
"are [weeks, days, hours, minutes, seconds, milliseconds, "
347-
"microseconds, nanoseconds]"
346+
"are ('weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds', "
347+
"'microseconds', 'nanoseconds')"
348348
)
349349

350350
with pytest.raises(ValueError, match=msg):

pandas/tests/scalar/timestamp/test_arithmetic.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_overflow_offset_raises(self):
7272
with pytest.raises(OverflowError, match=lmsg):
7373
stamp - offset_overflow
7474

75-
def test_sub_can_return_stdlib_timedelta_to_avoid_overflow(self, timedelta_overflow):
75+
def test_sub_returns_stdlib_timedelta_to_avoid_overflow(self, timedelta_overflow):
7676
# https://github.com/pandas-dev/pandas/issues/31774
7777
msg = "Result is too large for pandas.Timedelta"
7878
a = Timestamp("2101-01-01 00:00:00")
@@ -88,7 +88,6 @@ def test_sub_can_return_stdlib_timedelta_to_avoid_overflow(self, timedelta_overf
8888
assert isinstance(r0, timedelta)
8989
assert isinstance(r1, timedelta)
9090

91-
9291
def test_delta_preserve_nanos(self):
9392
val = Timestamp(1337299200000000123)
9493
result = val + timedelta(1)

pandas/tests/tools/test_to_datetime.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -1838,14 +1838,8 @@ def test_to_datetime_list_of_integers(self):
18381838
def test_to_datetime_overflow(self):
18391839
# gh-17637
18401840
# we are overflowing Timedelta range here
1841+
msg = "outside allowed range"
18411842

1842-
msg = "|".join(
1843-
[
1844-
"Python int too large to convert to C long",
1845-
"long too big to convert",
1846-
"int too big to convert",
1847-
]
1848-
)
18491843
with pytest.raises(OutOfBoundsTimedelta, match=msg):
18501844
date_range(start="1/1/1700", freq="B", periods=100000)
18511845

0 commit comments

Comments
 (0)