Skip to content

datetimelike indexes add/sub zero-dim integer arrays #19013

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

Merged
merged 5 commits into from
Dec 31, 2017
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ Numeric
^^^^^^^

- Bug in :func:`Series.__sub__` subtracting a non-nanosecond ``np.datetime64`` object from a ``Series`` gave incorrect results (:issue:`7996`)
-
- Bug in :class:`DatetimeIndex`, :class:`TimedeltaIndex` addition and subtraction of zero-dimensional integer arrays gave incrrect results (:issue:`19012`)
-

Categorical
Expand Down
4 changes: 3 additions & 1 deletion pandas/core/dtypes/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
ABCDatetimeIndex, ABCSeries,
ABCSparseArray, ABCSparseSeries, ABCCategoricalIndex,
ABCIndexClass, ABCDateOffset)
from .inference import is_string_like, is_list_like
from .inference import is_string_like, is_list_like, is_zero_dim_array
from .inference import * # noqa


Expand Down Expand Up @@ -291,6 +291,8 @@ def is_offsetlike(arr_or_obj):
"""
if isinstance(arr_or_obj, ABCDateOffset):
return True
elif is_zero_dim_array(arr_or_obj):
return isinstance(arr_or_obj.item(), ABCDateOffset)
elif (is_list_like(arr_or_obj) and len(arr_or_obj) and
is_object_dtype(arr_or_obj)):
return all(isinstance(x, ABCDateOffset) for x in arr_or_obj)
Expand Down
16 changes: 16 additions & 0 deletions pandas/core/dtypes/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@
is_interval = lib.is_interval


def is_zero_dim_array(obj):
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this already exists

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have item_from_zerodim. so need to reconcile this. you generally don't need to actually check, rather you just unbox.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In [4]: pd.api.types.is_scalar(np.int64(1))
Out[4]: True

Check if this is a numpy array with dimension zero, which in some
cases is treated like a scalar.

Parameters
----------
obj : object

Returns
-------
is_zero_dim_array : bool
"""
return isinstance(obj, np.ndarray) and obj.ndim == 0


def is_number(obj):
"""
Check if the object is a number.
Expand Down
8 changes: 5 additions & 3 deletions pandas/core/indexes/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
is_dtype_equal,
is_float,
is_integer,
is_list_like,
is_list_like, is_zero_dim_array,
is_scalar,
is_bool_dtype,
is_offsetlike,
Expand Down Expand Up @@ -678,7 +678,8 @@ def __add__(self, other):
.format(typ=type(other)))
elif isinstance(other, (DateOffset, timedelta)):
return self._add_delta(other)
elif is_integer(other):
elif is_integer(other) or (is_integer_dtype(other) and
is_zero_dim_array(other)):
return self.shift(other)
elif isinstance(other, (datetime, np.datetime64)):
return self._add_datelike(other)
Expand Down Expand Up @@ -708,7 +709,8 @@ def __sub__(self, other):
return self._sub_datelike(other)
elif isinstance(other, (DateOffset, timedelta)):
return self._add_delta(-other)
elif is_integer(other):
elif is_integer(other) or (is_integer_dtype(other) and
is_zero_dim_array(other)):
return self.shift(-other)
elif isinstance(other, (datetime, np.datetime64)):
return self._sub_datelike(other)
Expand Down
9 changes: 9 additions & 0 deletions pandas/tests/dtypes/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
is_integer,
is_float,
is_bool,
is_zero_dim_array,
is_scalar,
is_scipy_sparse,
_ensure_int32,
Expand Down Expand Up @@ -1218,3 +1219,11 @@ def test_ensure_categorical():
values = Categorical(values)
result = _ensure_categorical(values)
tm.assert_categorical_equal(result, values)


def test_is_zero_dim_array():
assert not is_zero_dim_array(1)
assert not is_zero_dim_array(False)
assert not is_zero_dim_array(np.array([]))
assert not is_zero_dim_array(np.array([1]))
assert is_zero_dim_array(np.array(1))
7 changes: 7 additions & 0 deletions pandas/tests/indexes/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
import numpy as np

import pandas.util.testing as tm
from pandas.core.indexes.api import Index, MultiIndex
Expand All @@ -22,3 +23,9 @@
ids=lambda x: type(x).__name__)
def indices(request):
return request.param


@pytest.fixture(params=[1, np.array(1, dtype=np.int64)])
def one(request):
# zero-dim integer array behaves like an integer
return request.param
17 changes: 9 additions & 8 deletions pandas/tests/indexes/datetimes/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,36 +58,37 @@ def test_dti_radd_timestamp_raises(self):
# -------------------------------------------------------------
# Binary operations DatetimeIndex and int

def test_dti_add_int(self, tz):
def test_dti_add_int(self, tz, one):
# Variants of `one` for #19012
rng = pd.date_range('2000-01-01 09:00', freq='H',
periods=10, tz=tz)
result = rng + 1
result = rng + one
expected = pd.date_range('2000-01-01 10:00', freq='H',
periods=10, tz=tz)
tm.assert_index_equal(result, expected)

def test_dti_iadd_int(self, tz):
def test_dti_iadd_int(self, tz, one):
rng = pd.date_range('2000-01-01 09:00', freq='H',
periods=10, tz=tz)
expected = pd.date_range('2000-01-01 10:00', freq='H',
periods=10, tz=tz)
rng += 1
rng += one
tm.assert_index_equal(rng, expected)

def test_dti_sub_int(self, tz):
def test_dti_sub_int(self, tz, one):
rng = pd.date_range('2000-01-01 09:00', freq='H',
periods=10, tz=tz)
result = rng - 1
result = rng - one
expected = pd.date_range('2000-01-01 08:00', freq='H',
periods=10, tz=tz)
tm.assert_index_equal(result, expected)

def test_dti_isub_int(self, tz):
def test_dti_isub_int(self, tz, one):
rng = pd.date_range('2000-01-01 09:00', freq='H',
periods=10, tz=tz)
expected = pd.date_range('2000-01-01 08:00', freq='H',
periods=10, tz=tz)
rng -= 1
rng -= one
tm.assert_index_equal(rng, expected)

# -------------------------------------------------------------
Expand Down
14 changes: 8 additions & 6 deletions pandas/tests/indexes/period/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,21 @@ def test_add_iadd(self):
period.IncompatibleFrequency, msg):
rng += delta

# int
def test_pi_add_int(self, one):
# Variants of `one` for #19012
rng = pd.period_range('2000-01-01 09:00', freq='H', periods=10)
result = rng + 1
result = rng + one
expected = pd.period_range('2000-01-01 10:00', freq='H', periods=10)
tm.assert_index_equal(result, expected)
rng += 1
rng += one
tm.assert_index_equal(rng, expected)

def test_sub(self):
@pytest.mark.parametrize('five', [5, np.array(5, dtype=np.int64)])
def test_sub(self, five):
rng = period_range('2007-01', periods=50)

result = rng - 5
exp = rng + (-5)
result = rng - five
exp = rng + (-five)
tm.assert_index_equal(result, exp)

def test_sub_isub(self):
Expand Down
17 changes: 9 additions & 8 deletions pandas/tests/indexes/timedeltas/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,28 +121,29 @@ def test_ufunc_coercions(self):
# -------------------------------------------------------------
# Binary operations TimedeltaIndex and integer

def test_tdi_add_int(self):
def test_tdi_add_int(self, one):
# Variants of `one` for #19012
rng = timedelta_range('1 days 09:00:00', freq='H', periods=10)
result = rng + 1
result = rng + one
expected = timedelta_range('1 days 10:00:00', freq='H', periods=10)
tm.assert_index_equal(result, expected)

def test_tdi_iadd_int(self):
def test_tdi_iadd_int(self, one):
rng = timedelta_range('1 days 09:00:00', freq='H', periods=10)
expected = timedelta_range('1 days 10:00:00', freq='H', periods=10)
rng += 1
rng += one
tm.assert_index_equal(rng, expected)

def test_tdi_sub_int(self):
def test_tdi_sub_int(self, one):
rng = timedelta_range('1 days 09:00:00', freq='H', periods=10)
result = rng - 1
result = rng - one
expected = timedelta_range('1 days 08:00:00', freq='H', periods=10)
tm.assert_index_equal(result, expected)

def test_tdi_isub_int(self):
def test_tdi_isub_int(self, one):
rng = timedelta_range('1 days 09:00:00', freq='H', periods=10)
expected = timedelta_range('1 days 08:00:00', freq='H', periods=10)
rng -= 1
rng -= one
tm.assert_index_equal(rng, expected)

# -------------------------------------------------------------
Expand Down