Skip to content

Commit cdebcf3

Browse files
jbrockmendeljreback
authored andcommitted
Refactor _TimeOp._validate to separate datetime vs timedelta vs dateoffset (#18832)
1 parent 81adbe6 commit cdebcf3

File tree

2 files changed

+115
-53
lines changed

2 files changed

+115
-53
lines changed

pandas/core/ops.py

+72-43
Original file line numberDiff line numberDiff line change
@@ -387,54 +387,19 @@ def __init__(self, left, right, name, na_op):
387387
self.lvalues, self.rvalues = self._convert_for_datetime(lvalues,
388388
rvalues)
389389

390-
def _validate(self, lvalues, rvalues, name):
391-
# timedelta and integer mul/div
392-
393-
if ((self.is_timedelta_lhs and
394-
(self.is_integer_rhs or self.is_floating_rhs)) or
395-
(self.is_timedelta_rhs and
396-
(self.is_integer_lhs or self.is_floating_lhs))):
397-
398-
if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'):
399-
raise TypeError("can only operate on a timedelta and an "
400-
"integer or a float for division and "
401-
"multiplication, but the operator [{name}] "
402-
"was passed".format(name=name))
403-
404-
# 2 timedeltas
405-
elif ((self.is_timedelta_lhs and
406-
(self.is_timedelta_rhs or self.is_offset_rhs)) or
407-
(self.is_timedelta_rhs and
408-
(self.is_timedelta_lhs or self.is_offset_lhs))):
409-
410-
if name not in ('__div__', '__rdiv__', '__truediv__',
411-
'__rtruediv__', '__add__', '__radd__', '__sub__',
412-
'__rsub__'):
413-
raise TypeError("can only operate on a timedeltas for addition"
414-
", subtraction, and division, but the operator"
415-
" [{name}] was passed".format(name=name))
416-
417-
# datetime and timedelta/DateOffset
418-
elif (self.is_datetime_lhs and
419-
(self.is_timedelta_rhs or self.is_offset_rhs)):
390+
def _validate_datetime(self, lvalues, rvalues, name):
391+
# assumes self.is_datetime_lhs
420392

393+
if (self.is_timedelta_rhs or self.is_offset_rhs):
394+
# datetime and timedelta/DateOffset
421395
if name not in ('__add__', '__radd__', '__sub__'):
422396
raise TypeError("can only operate on a datetime with a rhs of "
423397
"a timedelta/DateOffset for addition and "
424398
"subtraction, but the operator [{name}] was "
425399
"passed".format(name=name))
426400

427-
elif (self.is_datetime_rhs and
428-
(self.is_timedelta_lhs or self.is_offset_lhs)):
429-
if name not in ('__add__', '__radd__', '__rsub__'):
430-
raise TypeError("can only operate on a timedelta/DateOffset "
431-
"with a rhs of a datetime for addition, "
432-
"but the operator [{name}] was passed"
433-
.format(name=name))
434-
435-
# 2 datetimes
436-
elif self.is_datetime_lhs and self.is_datetime_rhs:
437-
401+
elif self.is_datetime_rhs:
402+
# 2 datetimes
438403
if name not in ('__sub__', '__rsub__'):
439404
raise TypeError("can only operate on a datetimes for"
440405
" subtraction, but the operator [{name}] was"
@@ -445,18 +410,82 @@ def _validate(self, lvalues, rvalues, name):
445410
raise ValueError("Incompatible tz's on datetime subtraction "
446411
"ops")
447412

448-
elif ((self.is_timedelta_lhs or self.is_offset_lhs) and
449-
self.is_datetime_rhs):
413+
else:
414+
raise TypeError('cannot operate on a series without a rhs '
415+
'of a series/ndarray of type datetime64[ns] '
416+
'or a timedelta')
417+
418+
def _validate_timedelta(self, name):
419+
# assumes self.is_timedelta_lhs
420+
421+
if self.is_integer_rhs or self.is_floating_rhs:
422+
# timedelta and integer mul/div
423+
self._check_timedelta_with_numeric(name)
424+
elif self.is_timedelta_rhs or self.is_offset_rhs:
425+
# 2 timedeltas
426+
if name not in ('__div__', '__rdiv__', '__truediv__',
427+
'__rtruediv__', '__add__', '__radd__', '__sub__',
428+
'__rsub__'):
429+
raise TypeError("can only operate on a timedeltas for addition"
430+
", subtraction, and division, but the operator"
431+
" [{name}] was passed".format(name=name))
432+
elif self.is_datetime_rhs:
433+
if name not in ('__add__', '__radd__', '__rsub__'):
434+
raise TypeError("can only operate on a timedelta/DateOffset "
435+
"with a rhs of a datetime for addition, "
436+
"but the operator [{name}] was passed"
437+
.format(name=name))
438+
else:
439+
raise TypeError('cannot operate on a series without a rhs '
440+
'of a series/ndarray of type datetime64[ns] '
441+
'or a timedelta')
442+
443+
def _validate_offset(self, name):
444+
# assumes self.is_offset_lhs
445+
446+
if self.is_timedelta_rhs:
447+
# 2 timedeltas
448+
if name not in ('__div__', '__rdiv__', '__truediv__',
449+
'__rtruediv__', '__add__', '__radd__', '__sub__',
450+
'__rsub__'):
451+
raise TypeError("can only operate on a timedeltas for addition"
452+
", subtraction, and division, but the operator"
453+
" [{name}] was passed".format(name=name))
450454

455+
elif self.is_datetime_rhs:
451456
if name not in ('__add__', '__radd__'):
452457
raise TypeError("can only operate on a timedelta/DateOffset "
453458
"and a datetime for addition, but the operator"
454459
" [{name}] was passed".format(name=name))
460+
455461
else:
456462
raise TypeError('cannot operate on a series without a rhs '
457463
'of a series/ndarray of type datetime64[ns] '
458464
'or a timedelta')
459465

466+
def _validate(self, lvalues, rvalues, name):
467+
if self.is_datetime_lhs:
468+
return self._validate_datetime(lvalues, rvalues, name)
469+
elif self.is_timedelta_lhs:
470+
return self._validate_timedelta(name)
471+
elif self.is_offset_lhs:
472+
return self._validate_offset(name)
473+
474+
if ((self.is_integer_lhs or self.is_floating_lhs) and
475+
self.is_timedelta_rhs):
476+
self._check_timedelta_with_numeric(name)
477+
else:
478+
raise TypeError('cannot operate on a series without a rhs '
479+
'of a series/ndarray of type datetime64[ns] '
480+
'or a timedelta')
481+
482+
def _check_timedelta_with_numeric(self, name):
483+
if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'):
484+
raise TypeError("can only operate on a timedelta and an "
485+
"integer or a float for division and "
486+
"multiplication, but the operator [{name}] "
487+
"was passed".format(name=name))
488+
460489
def _convert_to_array(self, values, name=None, other=None):
461490
"""converts values to ndarray"""
462491
from pandas.core.tools.timedeltas import to_timedelta

pandas/tests/series/test_operators.py

+43-10
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,51 @@ def test_timedelta64_ops_nat(self):
960960
assert_series_equal(timedelta_series / nan,
961961
nat_series_dtype_timedelta)
962962

963+
@pytest.mark.parametrize('scalar_td', [timedelta(minutes=5, seconds=4),
964+
Timedelta(minutes=5, seconds=4),
965+
Timedelta('5m4s').to_timedelta64()])
966+
def test_operators_timedelta64_with_timedelta(self, scalar_td):
967+
# smoke tests
968+
td1 = Series([timedelta(minutes=5, seconds=3)] * 3)
969+
td1.iloc[2] = np.nan
970+
971+
td1 + scalar_td
972+
scalar_td + td1
973+
td1 - scalar_td
974+
scalar_td - td1
975+
td1 / scalar_td
976+
scalar_td / td1
977+
978+
@pytest.mark.parametrize('scalar_td', [
979+
timedelta(minutes=5, seconds=4),
980+
pytest.param(Timedelta('5m4s'),
981+
marks=pytest.mark.xfail(reason="Timedelta.__floordiv__ "
982+
"bug GH#18846")),
983+
Timedelta('5m4s').to_timedelta64()])
984+
def test_operators_timedelta64_with_timedelta_invalid(self, scalar_td):
985+
td1 = Series([timedelta(minutes=5, seconds=3)] * 3)
986+
td1.iloc[2] = np.nan
987+
988+
# check that we are getting a TypeError
989+
# with 'operate' (from core/ops.py) for the ops that are not
990+
# defined
991+
pattern = 'operate|unsupported|cannot'
992+
with tm.assert_raises_regex(TypeError, pattern):
993+
td1 * scalar_td
994+
with tm.assert_raises_regex(TypeError, pattern):
995+
scalar_td * td1
996+
with tm.assert_raises_regex(TypeError, pattern):
997+
td1 // scalar_td
998+
with tm.assert_raises_regex(TypeError, pattern):
999+
scalar_td // td1
1000+
with tm.assert_raises_regex(TypeError, pattern):
1001+
scalar_td ** td1
1002+
with tm.assert_raises_regex(TypeError, pattern):
1003+
td1 ** scalar_td
1004+
9631005

9641006
class TestDatetimeSeriesArithmetic(object):
1007+
9651008
def test_operators_datetimelike(self):
9661009
def run_ops(ops, get_ser, test_ser):
9671010

@@ -976,16 +1019,6 @@ def run_ops(ops, get_ser, test_ser):
9761019
# ## timedelta64 ###
9771020
td1 = Series([timedelta(minutes=5, seconds=3)] * 3)
9781021
td1.iloc[2] = np.nan
979-
td2 = timedelta(minutes=5, seconds=4)
980-
ops = ['__mul__', '__floordiv__', '__pow__', '__rmul__',
981-
'__rfloordiv__', '__rpow__']
982-
run_ops(ops, td1, td2)
983-
td1 + td2
984-
td2 + td1
985-
td1 - td2
986-
td2 - td1
987-
td1 / td2
988-
td2 / td1
9891022

9901023
# ## datetime64 ###
9911024
dt1 = Series([Timestamp('20111230'), Timestamp('20120101'),

0 commit comments

Comments
 (0)