diff --git a/pandas/_libs/tslibs/vectorized.pyi b/pandas/_libs/tslibs/vectorized.pyi index 8820a17ce5996..72a87095e847c 100644 --- a/pandas/_libs/tslibs/vectorized.pyi +++ b/pandas/_libs/tslibs/vectorized.pyi @@ -14,6 +14,7 @@ def dt64arr_to_periodarr( stamps: npt.NDArray[np.int64], freq: int, tz: tzinfo | None, + reso: int = ..., # NPY_DATETIMEUNIT ) -> npt.NDArray[np.int64]: ... def is_date_array_normalized( stamps: npt.NDArray[np.int64], diff --git a/pandas/_libs/tslibs/vectorized.pyx b/pandas/_libs/tslibs/vectorized.pyx index 2cab55e607f15..3686b330fb6c2 100644 --- a/pandas/_libs/tslibs/vectorized.pyx +++ b/pandas/_libs/tslibs/vectorized.pyx @@ -33,6 +33,7 @@ from .np_datetime cimport ( NPY_FR_ns, dt64_to_dtstruct, npy_datetimestruct, + pandas_datetime_to_datetimestruct, ) from .offsets cimport BaseOffset from .period cimport get_period_ordinal @@ -354,10 +355,12 @@ def is_date_array_normalized(ndarray stamps, tzinfo tz, NPY_DATETIMEUNIT reso) - @cython.wraparound(False) @cython.boundscheck(False) -def dt64arr_to_periodarr(ndarray stamps, int freq, tzinfo tz): +def dt64arr_to_periodarr( + ndarray stamps, int freq, tzinfo tz, NPY_DATETIMEUNIT reso=NPY_FR_ns +): # stamps is int64_t, arbitrary ndim cdef: - Localizer info = Localizer(tz, reso=NPY_FR_ns) + Localizer info = Localizer(tz, reso=reso) Py_ssize_t i, n = stamps.size Py_ssize_t pos = -1 # unused, avoid not-initialized warning int64_t utc_val, local_val, res_val @@ -374,7 +377,7 @@ def dt64arr_to_periodarr(ndarray stamps, int freq, tzinfo tz): res_val = NPY_NAT else: local_val = info.utc_val_to_local_val(utc_val, &pos) - dt64_to_dtstruct(local_val, &dts) + pandas_datetime_to_datetimestruct(local_val, reso, &dts) res_val = get_period_ordinal(&dts, freq) # Analogous to: result[i] = res_val diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index b6d21cd9dac54..c40e9428bab25 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -22,6 +22,7 @@ astype_overflowsafe, delta_to_nanoseconds, dt64arr_to_periodarr as c_dt64arr_to_periodarr, + get_unit_from_dtype, iNaT, parsing, period as libperiod, @@ -1024,7 +1025,7 @@ def dt64arr_to_periodarr(data, freq, tz=None): used. """ - if data.dtype != np.dtype("M8[ns]"): + if not isinstance(data.dtype, np.dtype) or data.dtype.kind != "M": raise ValueError(f"Wrong dtype: {data.dtype}") if freq is None: @@ -1036,9 +1037,10 @@ def dt64arr_to_periodarr(data, freq, tz=None): elif isinstance(data, (ABCIndex, ABCSeries)): data = data._values + reso = get_unit_from_dtype(data.dtype) freq = Period._maybe_convert_freq(freq) base = freq._period_dtype_code - return c_dt64arr_to_periodarr(data.view("i8"), base, tz), freq + return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq def _get_ordinal_range(start, end, periods, freq, mult=1): diff --git a/pandas/tests/arrays/test_datetimes.py b/pandas/tests/arrays/test_datetimes.py index f3d471ca96614..d4f8e5b76a7c5 100644 --- a/pandas/tests/arrays/test_datetimes.py +++ b/pandas/tests/arrays/test_datetimes.py @@ -37,6 +37,26 @@ def dtype(self, unit, tz_naive_fixture): else: return DatetimeTZDtype(unit=unit, tz=tz) + @pytest.fixture + def dta_dti(self, unit, dtype): + tz = getattr(dtype, "tz", None) + + dti = pd.date_range("2016-01-01", periods=55, freq="D", tz=tz) + if tz is None: + arr = np.asarray(dti).astype(f"M8[{unit}]") + else: + arr = np.asarray(dti.tz_convert("UTC").tz_localize(None)).astype( + f"M8[{unit}]" + ) + + dta = DatetimeArray._simple_new(arr, dtype=dtype) + return dta, dti + + @pytest.fixture + def dta(self, dta_dti): + dta, dti = dta_dti + return dta + def test_non_nano(self, unit, reso, dtype): arr = np.arange(5, dtype=np.int64).view(f"M8[{unit}]") dta = DatetimeArray._simple_new(arr, dtype=dtype) @@ -52,17 +72,8 @@ def test_non_nano(self, unit, reso, dtype): @pytest.mark.parametrize( "field", DatetimeArray._field_ops + DatetimeArray._bool_ops ) - def test_fields(self, unit, reso, field, dtype): - tz = getattr(dtype, "tz", None) - dti = pd.date_range("2016-01-01", periods=55, freq="D", tz=tz) - if tz is None: - arr = np.asarray(dti).astype(f"M8[{unit}]") - else: - arr = np.asarray(dti.tz_convert("UTC").tz_localize(None)).astype( - f"M8[{unit}]" - ) - - dta = DatetimeArray._simple_new(arr, dtype=dtype) + def test_fields(self, unit, reso, field, dtype, dta_dti): + dta, dti = dta_dti # FIXME: assert (dti == dta).all() @@ -107,6 +118,14 @@ def test_std_non_nano(self, unit): assert res._reso == dta._reso assert res == dti.std().floor(unit) + @pytest.mark.filterwarnings("ignore:Converting to PeriodArray.*:UserWarning") + def test_to_period(self, dta_dti): + dta, dti = dta_dti + result = dta.to_period("D") + expected = dti._data.to_period("D") + + tm.assert_extension_array_equal(result, expected) + class TestDatetimeArrayComparisons: # TODO: merge this into tests/arithmetic/test_datetime64 once it is diff --git a/pandas/tests/indexes/period/test_constructors.py b/pandas/tests/indexes/period/test_constructors.py index fdc286ef7ec1a..5dff5c2ad9c86 100644 --- a/pandas/tests/indexes/period/test_constructors.py +++ b/pandas/tests/indexes/period/test_constructors.py @@ -183,9 +183,10 @@ def test_constructor_datetime64arr(self): vals = np.arange(100000, 100000 + 10000, 100, dtype=np.int64) vals = vals.view(np.dtype("M8[us]")) - msg = r"Wrong dtype: datetime64\[us\]" - with pytest.raises(ValueError, match=msg): - PeriodIndex(vals, freq="D") + pi = PeriodIndex(vals, freq="D") + + expected = PeriodIndex(vals.astype("M8[ns]"), freq="D") + tm.assert_index_equal(pi, expected) @pytest.mark.parametrize("box", [None, "series", "index"]) def test_constructor_datetime64arr_ok(self, box): diff --git a/setup.py b/setup.py index cb713e6d74392..27e6d8cb10025 100755 --- a/setup.py +++ b/setup.py @@ -551,7 +551,11 @@ def srcpath(name=None, suffix=".pyx", subdir="src"): "depends": tseries_depends, "sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"], }, - "_libs.tslibs.vectorized": {"pyxfile": "_libs/tslibs/vectorized"}, + "_libs.tslibs.vectorized": { + "pyxfile": "_libs/tslibs/vectorized", + "depends": tseries_depends, + "sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"], + }, "_libs.testing": {"pyxfile": "_libs/testing"}, "_libs.window.aggregations": { "pyxfile": "_libs/window/aggregations",