Skip to content

Commit e9dd35b

Browse files
committed
masking and overflow checks for datetimeindex and timedeltaindex ops
WIP filling out a test matrix of arithmetic ops closes pandas-dev#17991
1 parent 5959ee3 commit e9dd35b

File tree

3 files changed

+186
-3
lines changed

3 files changed

+186
-3
lines changed

pandas/core/indexes/datetimes.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import pandas.core.dtypes.concat as _concat
3030
from pandas.errors import PerformanceWarning
3131
from pandas.core.common import _values_from_object, _maybe_box
32+
from pandas.core.algorithms import checked_add_with_arr
3233

3334
from pandas.core.indexes.base import Index, _index_shared_docs
3435
from pandas.core.indexes.numeric import Int64Index, Float64Index
@@ -777,7 +778,8 @@ def _sub_datelike(self, other):
777778
"timezones or no timezones")
778779
else:
779780
i8 = self.asi8
780-
result = i8 - other.value
781+
result = checked_add_with_arr(i8, -other.value,
782+
arr_mask=self._isnan)
781783
result = self._maybe_mask_results(result,
782784
fill_value=libts.iNaT)
783785
else:

pandas/core/indexes/timedeltas.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,8 @@ def _add_datelike(self, other):
361361
else:
362362
other = Timestamp(other)
363363
i8 = self.asi8
364-
result = checked_add_with_arr(i8, other.value)
364+
result = checked_add_with_arr(i8, other.value,
365+
arr_mask=self._isnan)
365366
result = self._maybe_mask_results(result, fill_value=iNaT)
366367
return DatetimeIndex(result, name=self.name, copy=False)
367368

pandas/tests/indexes/datetimelike.py

+181-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
""" generic datetimelike tests """
22

3+
import pytest
4+
5+
import pandas as pd
36
from .common import Base
47
import pandas.util.testing as tm
5-
8+
from pandas import Timestamp, Timedelta, NaT
69

710
class DatetimeLike(Base):
811

@@ -38,3 +41,180 @@ def test_view(self, indices):
3841
i_view = i.view(self._holder)
3942
result = self._holder(i)
4043
tm.assert_index_equal(result, i_view)
44+
45+
46+
class TestDatetimeLikeIndexArithmetic(object):
47+
# GH17991 checking for overflows and NaT masking on arithmetic ops
48+
49+
# TODO: Fill out the matrix of allowed arithmetic operations:
50+
# - __rsub__, __radd__
51+
# - ops with scalars boxed in Index/Series/DataFrame
52+
# - ops with scalars:
53+
# NaT, Timestamp.min/max, Timedelta.min/max
54+
# datetime, timedelta, date(?),
55+
# relativedelta,
56+
# np.datetime64, np.timedelta64,
57+
# DateOffset,
58+
# Period
59+
# - timezone-aware variants
60+
# - PeriodIndex
61+
# - consistency with .map(...) ?
62+
63+
def test_timedeltaindex_add_timestamp_nat_masking(self):
64+
tdinat = pd.to_timedelta(['24658 days 11:15:00', 'NaT'])
65+
66+
# tsneg.value < 0, tspos.value > 0
67+
tsneg = Timestamp('1950-01-01')
68+
tspos = Timestamp('1980-01-01')
69+
70+
res1 = tdinat + tsneg
71+
assert res1[1] is NaT
72+
res2 = tdinat + tspos
73+
assert res2[1] is NaT
74+
75+
def test_timedeltaindex_sub_timestamp_nat_masking(self):
76+
tdinat = pd.to_timedelta(['24658 days 11:15:00', 'NaT'])
77+
78+
# tsneg.value < 0, tspos.value > 0
79+
tsneg = Timestamp('1950-01-01')
80+
tspos = Timestamp('1980-01-01')
81+
82+
res1 = tdinat - tsneg
83+
assert res1[1] is NaT
84+
res2 = tdinat - tspos
85+
assert res2[1] is NaT
86+
87+
def test_timedeltaindex_add_timestamp_overflow(self):
88+
tdimax = pd.to_timedelta(['24658 days 11:15:00', Timedelta.max])
89+
tdimin = pd.to_timedelta(['24658 days 11:15:00', Timedelta.min])
90+
91+
# tsneg.value < 0, tspos.value > 0
92+
tsneg = Timestamp('1950-01-01')
93+
tspos = Timestamp('1980-01-01')
94+
95+
res1 = tdimax + tsneg
96+
res2 = tdimin + tspos
97+
98+
with pytest.raises(OverflowError):
99+
tdimax + tspos
100+
101+
with pytest.raises(OverflowError):
102+
tdimin + tsneg
103+
104+
def test_timedeltaindex_add_timedelta_overflow(self):
105+
tdimax = pd.to_timedelta(['24658 days 11:15:00', Timedelta.max])
106+
tdimin = pd.to_timedelta(['24658 days 11:15:00', Timedelta.min])
107+
108+
# tdpos.value > 0, tdneg.value < 0
109+
tdpos = Timedelta('1h')
110+
tdneg = Timedelta('-1h')
111+
112+
with pytest.raises(OverflowError):
113+
res1 = tdimax + tdpos
114+
115+
res2 = tdimax + tdneg
116+
117+
res3 = tdimin + tdpos
118+
with pytest.raises(OverflowError):
119+
res4 = tdimin + tdneg
120+
121+
def test_timedeltaindex_sub_timedelta_overflow(self):
122+
tdimax = pd.to_timedelta(['24658 days 11:15:00', Timedelta.max])
123+
tdimin = pd.to_timedelta(['24658 days 11:15:00', Timedelta.min])
124+
125+
# tdpos.value > 0, tdneg.value < 0
126+
tdpos = Timedelta('1h')
127+
tdneg = Timedelta('-1h')
128+
129+
res1 = tdimax - tdpos
130+
with pytest.raises(OverflowError):
131+
res2 = tdimax - tdneg
132+
with pytest.raises(OverflowError):
133+
res3 = tdimin - tdpos
134+
res4 = tdimin - tdneg
135+
136+
def test_datetimeindex_add_nat_masking(self):
137+
# Checking for NaTs and checking that we don't get an OverflowError
138+
dtinat = pd.to_datetime(['now', 'NaT'])
139+
140+
# tdpos.value > 0, tdneg.value < 0
141+
tdpos = Timedelta('1h')
142+
tdneg = Timedelta('-1h')
143+
144+
res1 = dtinat + tdpos
145+
assert res1[1] is NaT
146+
res2 = dtinat + tdneg
147+
assert res2[1] is NaT
148+
149+
def test_datetimeindex_sub_nat_masking(self):
150+
# Checking for NaTs and checking that we don't get an OverflowError
151+
dtinat = pd.to_datetime(['now', 'NaT'])
152+
153+
# tdpos.value > 0, tdneg.value < 0
154+
tdpos = Timedelta('1h')
155+
tdneg = Timedelta('-1h')
156+
157+
res1 = dtinat - tdpos
158+
assert res1[1] is NaT
159+
res2 = dtinat - tdneg
160+
assert res2[1] is NaT
161+
162+
def test_datetimeindex_add_timedelta_overflow(self):
163+
dtimax = pd.to_datetime(['now', Timestamp.max])
164+
dtimin = pd.to_datetime(['now', Timestamp.min])
165+
166+
# tdpos.value < 0, tdneg.value > 0
167+
tdpos = Timedelta('1h')
168+
tdneg = Timedelta('-1h')
169+
170+
with pytest.raises(OverflowError):
171+
res1 = dtimax + tdpos
172+
173+
res2 = dtimax + tdneg
174+
assert res2[1].value == Timestamp.max.value + tdneg.value
175+
176+
res3 = dtimin + tdpos
177+
assert res3[1].value == Timestamp.min.value + tdpos.value
178+
179+
with pytest.raises(OverflowError):
180+
res4 = dtimin + tdneg
181+
182+
def test_datetimeindex_sub_timedelta_overflow(self):
183+
dtimax = pd.to_datetime(['now', Timestamp.max])
184+
dtimin = pd.to_datetime(['now', Timestamp.min])
185+
186+
# tdpos.value < 0, tdneg.value > 0
187+
tdpos = Timedelta('1h')
188+
tdneg = Timedelta('-1h')
189+
190+
res1 = dtimax - tdpos
191+
assert res1[1].value == Timestamp.max.value - tdpos.value
192+
193+
with pytest.raises(OverflowError):
194+
res2 = dtimax - tdneg
195+
196+
with pytest.raises(OverflowError):
197+
res3 = dtimin - tdpos
198+
199+
res4 = dtimin - tdneg
200+
assert res4[1].value == Timestamp.min.value - tdneg.value
201+
202+
def test_datetimeindex_sub_timestamp_overflow(self):
203+
dtimax = pd.to_datetime(['now', Timestamp.max])
204+
dtimin = pd.to_datetime(['now', Timestamp.min])
205+
206+
# tsneg.value < 0, tspos.value > 0
207+
tsneg = Timestamp('1950-01-01')
208+
tspos = Timestamp('1980-01-01')
209+
210+
with pytest.raises(OverflowError):
211+
res1 = dtimax - tsneg
212+
213+
res2 = dtimax - tspos
214+
assert res2[1].value == Timestamp.max.value - tspos.value
215+
216+
res3 = dtimin - tsneg
217+
assert res3[1].value == Timestamp.min.value - tsneg.value
218+
219+
with pytest.raises(OverflowError):
220+
res4 = dtimin - tspos

0 commit comments

Comments
 (0)