Skip to content

Commit ca9e8af

Browse files
committed
Fix and test casting tz-aware datetimeindex to object-dtype ndarray or Index
1 parent 937541e commit ca9e8af

File tree

5 files changed

+52
-5
lines changed

5 files changed

+52
-5
lines changed

doc/source/whatsnew/v0.24.0.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1357,3 +1357,5 @@ Other
13571357
- :meth:`~pandas.io.formats.style.Styler.bar` now also supports tablewise application (in addition to rowwise and columnwise) with ``axis=None`` and setting clipping range with ``vmin`` and ``vmax`` (:issue:`21548` and :issue:`21526`). ``NaN`` values are also handled properly.
13581358
- Logical operations ``&, |, ^`` between :class:`Series` and :class:`Index` will no longer raise ``ValueError`` (:issue:`22092`)
13591359
- Bug in :meth:`DataFrame.combine_first` in which column types were unexpectedly converted to float (:issue:`20699`)
1360+
- Bug in :class:`Index` where passing a timezone-aware :class:`DatetimeIndex` and `dtype=object` would incorrectly raise a ``ValueError`` (:issue:`????`)
1361+
- Bug in :class:`DatetimeIndex` where calling `np.array(dtindex, dtype=object)` would incorrectly return an array of ``long`` objects (:issue:`????`)

pandas/core/arrays/datetimes.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,16 @@ def _resolution(self):
374374
# ----------------------------------------------------------------
375375
# Array-like Methods
376376

377+
def __array__(self, dtype=None):
378+
if is_object_dtype(dtype):
379+
return np.array(list(self), dtype=object)
380+
elif dtype == 'i8':
381+
return self.asi8
382+
383+
# TODO: warn that dtype is not used?
384+
# warn that conversion may be lossy?
385+
return self._data.view(np.ndarray) # follow Index.__array__
386+
377387
def __iter__(self):
378388
"""
379389
Return an iterator over the boxed values

pandas/core/indexes/base.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,11 +301,15 @@ def __new__(cls, data=None, dtype=None, copy=False, name=None,
301301
(dtype is not None and is_datetime64_any_dtype(dtype)) or
302302
'tz' in kwargs):
303303
from pandas import DatetimeIndex
304-
result = DatetimeIndex(data, copy=copy, name=name,
305-
dtype=dtype, **kwargs)
304+
306305
if dtype is not None and is_dtype_equal(_o_dtype, dtype):
307-
return Index(result.to_pydatetime(), dtype=_o_dtype)
306+
# if `data` is already a TZaware DatetimeIndex, then passing
307+
# dtype=object to the constructor will raise spuriously
308+
result = DatetimeIndex(data, copy=copy, name=name, **kwargs)
309+
return Index(list(result), dtype=_o_dtype)
308310
else:
311+
result = DatetimeIndex(data, copy=copy, name=name,
312+
dtype=dtype, **kwargs)
309313
return result
310314

311315
elif (is_timedelta64_dtype(data) or

pandas/tests/arrays/test_datetimelike.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,29 @@ def timedelta_index(request):
5757

5858
class TestDatetimeArray(object):
5959

60+
def test_array_object_dtype(self, tz_naive_fixture):
61+
tz = tz_naive_fixture
62+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
63+
arr = DatetimeArrayMixin(dti)
64+
65+
expected = np.array(list(dti))
66+
67+
result = np.array(arr, dtype=object)
68+
tm.assert_numpy_array_equal(result, expected)
69+
70+
# also test the DatetimeIndex method while we're at it
71+
result = np.array(dti, dtype=object)
72+
tm.assert_numpy_array_equal(result, expected)
73+
74+
def test_array(self, tz_naive_fixture):
75+
tz = tz_naive_fixture
76+
dti = pd.date_range('2016-01-01', periods=3, tz=tz)
77+
arr = DatetimeArrayMixin(dti)
78+
79+
expected = dti.asi8.view('M8[ns]')
80+
result = np.array(arr)
81+
tm.assert_numpy_array_equal(result, expected)
82+
6083
def test_from_dti(self, tz_naive_fixture):
6184
tz = tz_naive_fixture
6285
dti = pd.date_range('2016-01-01', periods=3, tz=tz)

pandas/tests/indexes/test_base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def test_construction_list_tuples_nan(self, na_value, vtype):
132132
@pytest.mark.parametrize("cast_as_obj", [True, False])
133133
@pytest.mark.parametrize("index", [
134134
pd.date_range('2015-01-01 10:00', freq='D', periods=3,
135-
tz='US/Eastern'), # DTI with tz
135+
tz='US/Eastern', name='Green Eggs & Ham'), # DTI with tz
136136
pd.date_range('2015-01-01 10:00', freq='D', periods=3), # DTI no tz
137137
pd.timedelta_range('1 days', freq='D', periods=3), # td
138138
pd.period_range('2015-01-01', freq='D', periods=3) # period
@@ -145,8 +145,16 @@ def test_constructor_from_index_dtlike(self, cast_as_obj, index):
145145

146146
tm.assert_index_equal(result, index)
147147

148-
if isinstance(index, pd.DatetimeIndex) and hasattr(index, 'tz'):
148+
if isinstance(index, pd.DatetimeIndex):
149149
assert result.tz == index.tz
150+
if cast_as_obj:
151+
# GH#???? check that Index(dti, dtype=object) does not
152+
# incorrectly raise ValueError, and that nanoseconds are not
153+
# dropped
154+
index += pd.Timedelta(nanoseconds=50)
155+
result = pd.Index(index, dtype=object)
156+
assert result.dtype == np.object_
157+
assert list(result) == list(index)
150158

151159
@pytest.mark.parametrize("index,has_tz", [
152160
(pd.date_range('2015-01-01 10:00', freq='D', periods=3,

0 commit comments

Comments
 (0)