Skip to content

Commit 580151a

Browse files
committed
BUG/CLN: move .equals to DatetimeOpsMixin
1 parent 59524af commit 580151a

18 files changed

+177
-84
lines changed

doc/source/whatsnew/v0.19.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1454,6 +1454,7 @@ Bug Fixes
14541454
- Bug in operations on ``NaT`` returning ``float`` instead of ``datetime64[ns]`` (:issue:`12941`)
14551455
- Bug in ``Series`` flexible arithmetic methods (like ``.add()``) raises ``ValueError`` when ``axis=None`` (:issue:`13894`)
14561456
- Bug in ``DataFrame.to_csv()`` with ``MultiIndex`` columns in which a stray empty line was added (:issue:`6618`)
1457+
- Bug in ``DatetimeIndex``, ``TimedeltaIndex`` and ``PeriodIndex.equals()`` may return ``True`` when input isn't ``Index`` but contains the same values (:issue:`13107`)
14571458

14581459

14591460
- Bug in ``Index`` raises ``KeyError`` displaying incorrect column when column is not in the df and columns contains duplicate values (:issue:`13822`)

pandas/indexes/base.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1618,8 +1618,15 @@ def equals(self, other):
16181618
if not isinstance(other, Index):
16191619
return False
16201620

1621-
return array_equivalent(_values_from_object(self),
1622-
_values_from_object(other))
1621+
if is_object_dtype(self) and not is_object_dtype(other):
1622+
# if other is not object, use other's logic for coercion
1623+
return other.equals(self)
1624+
1625+
try:
1626+
return array_equivalent(_values_from_object(self),
1627+
_values_from_object(other))
1628+
except:
1629+
return False
16231630

16241631
def identical(self, other):
16251632
"""Similar to equals, but check that other comparable attributes are

pandas/indexes/category.py

+3
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,9 @@ def equals(self, other):
196196
if self.is_(other):
197197
return True
198198

199+
if not isinstance(other, Index):
200+
return False
201+
199202
try:
200203
other = self._is_dtype_compat(other)
201204
return array_equivalent(self._data, other)

pandas/indexes/multi.py

+4
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ def reindex(self, target, method=None, level=None, limit=None,
14361436
return_indexers=True,
14371437
keep_order=False)
14381438
else:
1439+
target = _ensure_index(target)
14391440
if self.equals(target):
14401441
indexer = None
14411442
else:
@@ -1984,6 +1985,9 @@ def equals(self, other):
19841985
if self.is_(other):
19851986
return True
19861987

1988+
if not isinstance(other, Index):
1989+
return False
1990+
19871991
if not isinstance(other, MultiIndex):
19881992
return array_equivalent(self._values,
19891993
_values_from_object(_ensure_index(other)))

pandas/indexes/numeric.py

+4-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pandas.types.common import (is_dtype_equal, pandas_dtype,
88
is_float_dtype, is_object_dtype,
99
is_integer_dtype, is_scalar)
10-
from pandas.types.missing import array_equivalent, isnull
10+
from pandas.types.missing import isnull
1111
from pandas.core.common import _values_from_object
1212

1313
from pandas import compat
@@ -160,16 +160,6 @@ def _convert_scalar_indexer(self, key, kind=None):
160160
return (super(Int64Index, self)
161161
._convert_scalar_indexer(key, kind=kind))
162162

163-
def equals(self, other):
164-
"""
165-
Determines if two Index objects contain the same elements.
166-
"""
167-
if self.is_(other):
168-
return True
169-
170-
return array_equivalent(_values_from_object(self),
171-
_values_from_object(other))
172-
173163
def _wrap_joined_index(self, joined, other):
174164
name = self.name if self.name == other.name else None
175165
return Int64Index(joined, name=name)
@@ -306,6 +296,9 @@ def equals(self, other):
306296
if self is other:
307297
return True
308298

299+
if not isinstance(other, Index):
300+
return False
301+
309302
# need to compare nans locations and make sure that they are the same
310303
# since nans don't compare equal this is a bit tricky
311304
try:

pandas/tests/indexes/common.py

+14
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,20 @@ def test_delete_base(self):
650650
# either depending on numpy version
651651
result = idx.delete(len(idx))
652652

653+
def test_equals(self):
654+
655+
for name, idx in compat.iteritems(self.indices):
656+
self.assertTrue(idx.equals(idx))
657+
self.assertTrue(idx.equals(idx.copy()))
658+
self.assertTrue(idx.equals(idx.astype(object)))
659+
660+
self.assertFalse(idx.equals(list(idx)))
661+
self.assertFalse(idx.equals(np.array(idx)))
662+
663+
if idx.nlevels == 1:
664+
# do not test MultiIndex
665+
self.assertFalse(idx.equals(pd.Series(idx)))
666+
653667
def test_equals_op(self):
654668
# GH9947, GH10637
655669
index_a = self.create_index()

pandas/tests/indexes/test_base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ def test_astype(self):
400400
casted = self.intIndex.astype('i8')
401401
self.assertEqual(casted.name, 'foobar')
402402

403-
def test_equals(self):
403+
def test_equals_object(self):
404404
# same
405405
self.assertTrue(Index(['a', 'b', 'c']).equals(Index(['a', 'b', 'c'])))
406406

pandas/tests/indexes/test_category.py

+24-13
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ def test_ensure_copied_data(self):
522522
result = CategoricalIndex(index.values, copy=False)
523523
self.assertIs(_base(index.values), _base(result.values))
524524

525-
def test_equals(self):
525+
def test_equals_categorical(self):
526526

527527
ci1 = CategoricalIndex(['a', 'b'], categories=['a', 'b'], ordered=True)
528528
ci2 = CategoricalIndex(['a', 'b'], categories=['a', 'b', 'c'],
@@ -556,19 +556,30 @@ def test_equals(self):
556556

557557
# tests
558558
# make sure that we are testing for category inclusion properly
559-
self.assertTrue(CategoricalIndex(
560-
list('aabca'), categories=['c', 'a', 'b']).equals(list('aabca')))
559+
ci = CategoricalIndex(list('aabca'), categories=['c', 'a', 'b'])
560+
self.assertFalse(ci.equals(list('aabca')))
561+
self.assertFalse(ci.equals(CategoricalIndex(list('aabca'))))
562+
self.assertTrue(ci.equals(ci.copy()))
563+
564+
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
565+
ci = CategoricalIndex(list('aabca'),
566+
categories=['c', 'a', 'b', np.nan])
567+
self.assertFalse(ci.equals(list('aabca')))
568+
self.assertFalse(ci.equals(CategoricalIndex(list('aabca'))))
561569
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
562-
self.assertTrue(CategoricalIndex(
563-
list('aabca'), categories=['c', 'a', 'b', np.nan]).equals(list(
564-
'aabca')))
565-
566-
self.assertFalse(CategoricalIndex(
567-
list('aabca') + [np.nan], categories=['c', 'a', 'b']).equals(list(
568-
'aabca')))
569-
self.assertTrue(CategoricalIndex(
570-
list('aabca') + [np.nan], categories=['c', 'a', 'b']).equals(list(
571-
'aabca') + [np.nan]))
570+
self.assertTrue(ci.equals(ci.copy()))
571+
572+
ci = CategoricalIndex(list('aabca') + [np.nan],
573+
categories=['c', 'a', 'b'])
574+
self.assertFalse(ci.equals(list('aabca')))
575+
self.assertFalse(ci.equals(CategoricalIndex(list('aabca'))))
576+
self.assertTrue(ci.equals(ci.copy()))
577+
578+
ci = CategoricalIndex(list('aabca') + [np.nan],
579+
categories=['c', 'a', 'b'])
580+
self.assertFalse(ci.equals(list('aabca') + [np.nan]))
581+
self.assertFalse(ci.equals(CategoricalIndex(list('aabca') + [np.nan])))
582+
self.assertTrue(ci.equals(ci.copy()))
572583

573584
def test_string_categorical_index_repr(self):
574585
# short

pandas/tests/indexes/test_multi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1232,7 +1232,7 @@ def test_to_hierarchical(self):
12321232
def test_bounds(self):
12331233
self.index._bounds
12341234

1235-
def test_equals(self):
1235+
def test_equals_multi(self):
12361236
self.assertTrue(self.index.equals(self.index))
12371237
self.assertTrue(self.index.equal_levels(self.index))
12381238

pandas/tests/indexes/test_numeric.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ def test_astype(self):
265265
i = Float64Index([0, 1.1, np.NAN])
266266
self.assertRaises(ValueError, lambda: i.astype(dtype))
267267

268-
def test_equals(self):
268+
def test_equals_numeric(self):
269269

270270
i = Float64Index([1.0, 2.0])
271271
self.assertTrue(i.equals(i))

pandas/tests/indexes/test_range.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ def test_is_monotonic(self):
337337
self.assertTrue(index.is_monotonic_increasing)
338338
self.assertTrue(index.is_monotonic_decreasing)
339339

340-
def test_equals(self):
340+
def test_equals_range(self):
341341
equiv_pairs = [(RangeIndex(0, 9, 2), RangeIndex(0, 10, 2)),
342342
(RangeIndex(0), RangeIndex(1, -1, 3)),
343343
(RangeIndex(1, 2, 3), RangeIndex(1, 3, 4)),

pandas/tseries/base.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import numpy as np
1212
from pandas.types.common import (is_integer, is_float,
1313
is_bool_dtype, _ensure_int64,
14-
is_scalar,
14+
is_scalar, is_dtype_equal,
1515
is_list_like)
1616
from pandas.types.generic import (ABCIndex, ABCSeries,
1717
ABCPeriodIndex, ABCIndexClass)
@@ -108,6 +108,34 @@ def ceil(self, freq):
108108
class DatetimeIndexOpsMixin(object):
109109
""" common ops mixin to support a unified inteface datetimelike Index """
110110

111+
def equals(self, other):
112+
"""
113+
Determines if two Index objects contain the same elements.
114+
"""
115+
if self.is_(other):
116+
return True
117+
118+
if not isinstance(other, ABCIndexClass):
119+
return False
120+
elif not isinstance(other, type(self)):
121+
try:
122+
other = type(self)(other)
123+
except:
124+
return False
125+
126+
if not is_dtype_equal(self.dtype, other.dtype):
127+
# have different timezone
128+
return False
129+
130+
# ToDo: Remove this when PeriodDtype is added
131+
elif isinstance(self, ABCPeriodIndex):
132+
if not isinstance(other, ABCPeriodIndex):
133+
return False
134+
if self.freq != other.freq:
135+
return False
136+
137+
return np.array_equal(self.asi8, other.asi8)
138+
111139
def __iter__(self):
112140
return (self._box_func(v) for v in self.asi8)
113141

pandas/tseries/index.py

-20
Original file line numberDiff line numberDiff line change
@@ -1655,26 +1655,6 @@ def is_normalized(self):
16551655
def _resolution(self):
16561656
return period.resolution(self.asi8, self.tz)
16571657

1658-
def equals(self, other):
1659-
"""
1660-
Determines if two Index objects contain the same elements.
1661-
"""
1662-
if self.is_(other):
1663-
return True
1664-
1665-
if (not hasattr(other, 'inferred_type') or
1666-
other.inferred_type != 'datetime64'):
1667-
if self.offset is not None:
1668-
return False
1669-
try:
1670-
other = DatetimeIndex(other)
1671-
except:
1672-
return False
1673-
1674-
if self._has_same_tz(other):
1675-
return np.array_equal(self.asi8, other.asi8)
1676-
return False
1677-
16781658
def insert(self, loc, item):
16791659
"""
16801660
Make new Index inserting new item at location

pandas/tseries/period.py

-15
Original file line numberDiff line numberDiff line change
@@ -596,21 +596,6 @@ def _mpl_repr(self):
596596
# how to represent ourselves to matplotlib
597597
return self.asobject.values
598598

599-
def equals(self, other):
600-
"""
601-
Determines if two Index objects contain the same elements.
602-
"""
603-
if self.is_(other):
604-
return True
605-
606-
if not isinstance(other, PeriodIndex):
607-
try:
608-
other = PeriodIndex(other)
609-
except:
610-
return False
611-
612-
return np.array_equal(self.asi8, other.asi8)
613-
614599
def to_timestamp(self, freq=None, how='start'):
615600
"""
616601
Cast to DatetimeIndex

pandas/tseries/tdi.py

-16
Original file line numberDiff line numberDiff line change
@@ -834,22 +834,6 @@ def dtype(self):
834834
def is_all_dates(self):
835835
return True
836836

837-
def equals(self, other):
838-
"""
839-
Determines if two Index objects contain the same elements.
840-
"""
841-
if self.is_(other):
842-
return True
843-
844-
if (not hasattr(other, 'inferred_type') or
845-
other.inferred_type != 'timedelta64'):
846-
try:
847-
other = TimedeltaIndex(other)
848-
except:
849-
return False
850-
851-
return np.array_equal(self.asi8, other.asi8)
852-
853837
def insert(self, loc, item):
854838
"""
855839
Make new Index inserting new item at location

0 commit comments

Comments
 (0)