diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 388c5dbf6a7ee..2175a18a88e73 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -326,6 +326,7 @@ Numeric Conversion ^^^^^^^^^^ - Bug in :meth:`Series.to_dict` with ``orient='records'`` now returns python native types (:issue:`25969`) +- Bug in :meth:`Series.view` and :meth:`Index.view` when converting between datetime-like (``datetime64[ns]``, ``datetime64[ns, tz]``, ``timedelta64``, ``period``) dtypes (:issue:`39788`) - - diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 5dd55ff0f1fa2..a6ee95e131559 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -44,6 +44,7 @@ ) from pandas._libs.tslibs.timestamps import integer_op_not_supported from pandas._typing import ( + ArrayLike, DatetimeLikeScalar, Dtype, DtypeObj, @@ -79,6 +80,10 @@ is_unsigned_integer_dtype, pandas_dtype, ) +from pandas.core.dtypes.dtypes import ( + DatetimeTZDtype, + PeriodDtype, +) from pandas.core.dtypes.missing import ( is_valid_na_for_dtype, isna, @@ -428,9 +433,30 @@ def astype(self, dtype, copy=True): else: return np.asarray(self, dtype=dtype) - def view(self, dtype: Optional[Dtype] = None): + def view(self, dtype: Optional[Dtype] = None) -> ArrayLike: + # We handle datetime64, datetime64tz, timedelta64, and period + # dtypes here. Everything else we pass through to the underlying + # ndarray. if dtype is None or dtype is self.dtype: return type(self)(self._ndarray, dtype=self.dtype) + + if isinstance(dtype, type): + # we sometimes pass non-dtype objects, e.g np.ndarray; + # pass those through to the underlying ndarray + return self._ndarray.view(dtype) + + dtype = pandas_dtype(dtype) + if isinstance(dtype, (PeriodDtype, DatetimeTZDtype)): + cls = dtype.construct_array_type() + return cls._simple_new(self.asi8, dtype=dtype) + elif dtype == "M8[ns]": + from pandas.core.arrays import DatetimeArray + + return DatetimeArray._simple_new(self.asi8, dtype=dtype) + elif dtype == "m8[ns]": + from pandas.core.arrays import TimedeltaArray + + return TimedeltaArray._simple_new(self.asi8.view("m8[ns]"), dtype=dtype) return self._ndarray.view(dtype=dtype) # ------------------------------------------------------------------ diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 71095b8f4113a..7d8d964f63496 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -793,6 +793,23 @@ def view(self, cls=None): # we need to see if we are subclassing an # index type here if cls is not None and not hasattr(cls, "_typ"): + dtype = cls + if isinstance(cls, str): + dtype = pandas_dtype(cls) + + if isinstance(dtype, (np.dtype, ExtensionDtype)) and needs_i8_conversion( + dtype + ): + if dtype.kind == "m" and dtype != "m8[ns]": + # e.g. m8[s] + return self._data.view(cls) + + arr = self._data.view("i8") + idx_cls = self._dtype_to_subclass(dtype) + arr_cls = idx_cls._data_cls + arr = arr_cls._simple_new(self._data.view("i8"), dtype=dtype) + return idx_cls._simple_new(arr, name=self.name) + result = self._data.view(cls) else: result = self._view() diff --git a/pandas/tests/series/methods/test_view.py b/pandas/tests/series/methods/test_view.py index 48f7b47f6d25a..f0069cdb9b79c 100644 --- a/pandas/tests/series/methods/test_view.py +++ b/pandas/tests/series/methods/test_view.py @@ -1,5 +1,10 @@ +import numpy as np +import pytest + from pandas import ( + Index, Series, + array as pd_array, date_range, ) import pandas._testing as tm @@ -19,3 +24,23 @@ def test_view_tz(self): ] ) tm.assert_series_equal(result, expected) + + @pytest.mark.parametrize( + "first", ["m8[ns]", "M8[ns]", "M8[ns, US/Central]", "period[D]"] + ) + @pytest.mark.parametrize( + "second", ["m8[ns]", "M8[ns]", "M8[ns, US/Central]", "period[D]"] + ) + @pytest.mark.parametrize("box", [Series, Index, pd_array]) + def test_view_between_datetimelike(self, first, second, box): + + dti = date_range("2016-01-01", periods=3) + + orig = box(dti) + obj = orig.view(first) + assert obj.dtype == first + tm.assert_numpy_array_equal(np.asarray(obj.view("i8")), dti.asi8) + + res = obj.view(second) + assert res.dtype == second + tm.assert_numpy_array_equal(np.asarray(obj.view("i8")), dti.asi8)