-
-
Notifications
You must be signed in to change notification settings - Fork 18.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Bug] Fix various DatetimeIndex comparison bugs #22074
Changes from all commits
da6878a
ba6fdd0
6e92dc2
16202d5
c4ab98c
92b74ec
23aa2a0
b6bd737
4b46d6b
3ac9102
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
import numpy as np | ||
from pytz import utc | ||
|
||
from pandas._libs import tslib | ||
from pandas._libs import lib, tslib | ||
from pandas._libs.tslib import Timestamp, NaT, iNaT | ||
from pandas._libs.tslibs import ( | ||
normalize_date, | ||
|
@@ -18,7 +18,7 @@ | |
|
||
from pandas.core.dtypes.common import ( | ||
_NS_DTYPE, | ||
is_datetimelike, | ||
is_object_dtype, | ||
is_datetime64tz_dtype, | ||
is_datetime64_dtype, | ||
is_timedelta64_dtype, | ||
|
@@ -29,6 +29,7 @@ | |
|
||
import pandas.core.common as com | ||
from pandas.core.algorithms import checked_add_with_arr | ||
from pandas.core import ops | ||
|
||
from pandas.tseries.frequencies import to_offset | ||
from pandas.tseries.offsets import Tick, Day, generate_range | ||
|
@@ -99,31 +100,40 @@ def wrapper(self, other): | |
meth = getattr(dtl.DatetimeLikeArrayMixin, opname) | ||
|
||
if isinstance(other, (datetime, np.datetime64, compat.string_types)): | ||
if isinstance(other, datetime): | ||
if isinstance(other, (datetime, np.datetime64)): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since we do this all over the place, thing about making a type for this (for in internal use only), and/or maybe a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've got a branch going looking at |
||
# GH#18435 strings get a pass from tzawareness compat | ||
self._assert_tzawareness_compat(other) | ||
|
||
other = _to_m8(other, tz=self.tz) | ||
try: | ||
other = _to_m8(other, tz=self.tz) | ||
except ValueError: | ||
# string that cannot be parsed to Timestamp | ||
return ops.invalid_comparison(self, other, op) | ||
|
||
result = meth(self, other) | ||
if isna(other): | ||
result.fill(nat_result) | ||
elif lib.is_scalar(other): | ||
return ops.invalid_comparison(self, other, op) | ||
else: | ||
if isinstance(other, list): | ||
# FIXME: This can break for object-dtype with mixed types | ||
other = type(self)(other) | ||
elif not isinstance(other, (np.ndarray, ABCIndexClass, ABCSeries)): | ||
# Following Timestamp convention, __eq__ is all-False | ||
# and __ne__ is all True, others raise TypeError. | ||
if opname == '__eq__': | ||
return np.zeros(shape=self.shape, dtype=bool) | ||
elif opname == '__ne__': | ||
return np.ones(shape=self.shape, dtype=bool) | ||
raise TypeError('%s type object %s' % | ||
(type(other), str(other))) | ||
|
||
if is_datetimelike(other): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This previously called (Really, |
||
return ops.invalid_comparison(self, other, op) | ||
|
||
if is_object_dtype(other): | ||
result = op(self.astype('O'), np.array(other)) | ||
elif not (is_datetime64_dtype(other) or | ||
is_datetime64tz_dtype(other)): | ||
# e.g. is_timedelta64_dtype(other) | ||
return ops.invalid_comparison(self, other, op) | ||
else: | ||
self._assert_tzawareness_compat(other) | ||
result = meth(self, np.asarray(other)) | ||
|
||
result = meth(self, np.asarray(other)) | ||
result = com.values_from_object(result) | ||
|
||
# Make sure to pass an array to result[...]; indexing with | ||
|
@@ -152,6 +162,10 @@ class DatetimeArrayMixin(dtl.DatetimeLikeArrayMixin): | |
'is_year_end', 'is_leap_year'] | ||
_object_ops = ['weekday_name', 'freq', 'tz'] | ||
|
||
# dummy attribute so that datetime.__eq__(DatetimeArray) defers | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. huh? why don't you just implement and raise NotIMplemented? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is is a quirk of the stdlib |
||
# by returning NotImplemented | ||
timetuple = None | ||
|
||
# ----------------------------------------------------------------- | ||
# Constructors | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -275,6 +275,20 @@ def test_comparison_tzawareness_compat(self, op): | |
with pytest.raises(TypeError): | ||
op(ts, dz) | ||
|
||
@pytest.mark.parametrize('op', [operator.eq, operator.ne, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pretty sure we have fixtures for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a fixture for the names |
||
operator.gt, operator.ge, | ||
operator.lt, operator.le]) | ||
@pytest.mark.parametrize('other', [datetime(2016, 1, 1), | ||
Timestamp('2016-01-01'), | ||
np.datetime64('2016-01-01')]) | ||
def test_scalar_comparison_tzawareness(self, op, other, tz_aware_fixture): | ||
tz = tz_aware_fixture | ||
dti = pd.date_range('2016-01-01', periods=2, tz=tz) | ||
with pytest.raises(TypeError): | ||
op(dti, other) | ||
with pytest.raises(TypeError): | ||
op(other, dti) | ||
|
||
@pytest.mark.parametrize('op', [operator.eq, operator.ne, | ||
operator.gt, operator.ge, | ||
operator.lt, operator.le]) | ||
|
@@ -290,12 +304,60 @@ def test_nat_comparison_tzawareness(self, op): | |
result = op(dti.tz_localize('US/Pacific'), pd.NaT) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
def test_dti_cmp_int_raises(self): | ||
rng = date_range('1/1/2000', periods=10) | ||
def test_dti_cmp_str(self, tz_naive_fixture): | ||
# GH#22074 | ||
# regardless of tz, we expect these comparisons are valid | ||
tz = tz_naive_fixture | ||
rng = date_range('1/1/2000', periods=10, tz=tz) | ||
other = '1/1/2000' | ||
|
||
result = rng == other | ||
expected = np.array([True] + [False] * 9) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng != other | ||
expected = np.array([False] + [True] * 9) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng < other | ||
expected = np.array([False] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng <= other | ||
expected = np.array([True] + [False] * 9) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng > other | ||
expected = np.array([False] + [True] * 9) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng >= other | ||
expected = np.array([True] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
@pytest.mark.parametrize('other', ['foo', 99, 4.0, | ||
object(), timedelta(days=2)]) | ||
def test_dti_cmp_scalar_invalid(self, other, tz_naive_fixture): | ||
# GH#22074 | ||
tz = tz_naive_fixture | ||
rng = date_range('1/1/2000', periods=10, tz=tz) | ||
|
||
result = rng == other | ||
expected = np.array([False] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = rng != other | ||
expected = np.array([True] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
# raise TypeError for now | ||
with pytest.raises(TypeError): | ||
rng < rng[3].value | ||
rng < other | ||
with pytest.raises(TypeError): | ||
rng <= other | ||
with pytest.raises(TypeError): | ||
rng > other | ||
with pytest.raises(TypeError): | ||
rng >= other | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anything worth checking error message-wise? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are pretty scattershot at the moment. I'll give some thought to how that can be addressed. |
||
|
||
def test_dti_cmp_list(self): | ||
rng = date_range('1/1/2000', periods=10) | ||
|
@@ -304,6 +366,57 @@ def test_dti_cmp_list(self): | |
expected = rng == rng | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
@pytest.mark.parametrize('other', [ | ||
pd.timedelta_range('1D', periods=10), | ||
pd.timedelta_range('1D', periods=10).to_series(), | ||
pd.timedelta_range('1D', periods=10).asi8.view('m8[ns]') | ||
], ids=lambda x: type(x).__name__) | ||
def test_dti_cmp_tdi_tzawareness(self, other): | ||
# GH#22074 | ||
# reversion test that we _don't_ call _assert_tzawareness_compat | ||
# when comparing against TimedeltaIndex | ||
dti = date_range('2000-01-01', periods=10, tz='Asia/Tokyo') | ||
|
||
result = dti == other | ||
expected = np.array([False] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
result = dti != other | ||
expected = np.array([True] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
with pytest.raises(TypeError): | ||
dti < other | ||
with pytest.raises(TypeError): | ||
dti <= other | ||
with pytest.raises(TypeError): | ||
dti > other | ||
with pytest.raises(TypeError): | ||
dti >= other | ||
|
||
def test_dti_cmp_object_dtype(self): | ||
# GH#22074 | ||
dti = date_range('2000-01-01', periods=10, tz='Asia/Tokyo') | ||
|
||
other = dti.astype('O') | ||
|
||
result = dti == other | ||
expected = np.array([True] * 10) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
other = dti.tz_localize(None) | ||
with pytest.raises(TypeError): | ||
# tzawareness failure | ||
dti != other | ||
|
||
other = np.array(list(dti[:5]) + [Timedelta(days=1)] * 5) | ||
result = dti == other | ||
expected = np.array([True] * 5 + [False] * 5) | ||
tm.assert_numpy_array_equal(result, expected) | ||
|
||
with pytest.raises(TypeError): | ||
dti >= other | ||
|
||
|
||
class TestDatetimeIndexArithmetic(object): | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should one be marked #7830 ?