Skip to content

Commit 25749d2

Browse files
authored
ENH: DTA.to_period support non-nano (#47324)
* ENH: DTA.to_period support non-nano * update test
1 parent e1df797 commit 25749d2

File tree

6 files changed

+50
-20
lines changed

6 files changed

+50
-20
lines changed

pandas/_libs/tslibs/vectorized.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ def dt64arr_to_periodarr(
1414
stamps: npt.NDArray[np.int64],
1515
freq: int,
1616
tz: tzinfo | None,
17+
reso: int = ..., # NPY_DATETIMEUNIT
1718
) -> npt.NDArray[np.int64]: ...
1819
def is_date_array_normalized(
1920
stamps: npt.NDArray[np.int64],

pandas/_libs/tslibs/vectorized.pyx

+6-3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ from .np_datetime cimport (
3333
NPY_FR_ns,
3434
dt64_to_dtstruct,
3535
npy_datetimestruct,
36+
pandas_datetime_to_datetimestruct,
3637
)
3738
from .offsets cimport BaseOffset
3839
from .period cimport get_period_ordinal
@@ -354,10 +355,12 @@ def is_date_array_normalized(ndarray stamps, tzinfo tz, NPY_DATETIMEUNIT reso) -
354355

355356
@cython.wraparound(False)
356357
@cython.boundscheck(False)
357-
def dt64arr_to_periodarr(ndarray stamps, int freq, tzinfo tz):
358+
def dt64arr_to_periodarr(
359+
ndarray stamps, int freq, tzinfo tz, NPY_DATETIMEUNIT reso=NPY_FR_ns
360+
):
358361
# stamps is int64_t, arbitrary ndim
359362
cdef:
360-
Localizer info = Localizer(tz, reso=NPY_FR_ns)
363+
Localizer info = Localizer(tz, reso=reso)
361364
Py_ssize_t i, n = stamps.size
362365
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
363366
int64_t utc_val, local_val, res_val
@@ -374,7 +377,7 @@ def dt64arr_to_periodarr(ndarray stamps, int freq, tzinfo tz):
374377
res_val = NPY_NAT
375378
else:
376379
local_val = info.utc_val_to_local_val(utc_val, &pos)
377-
dt64_to_dtstruct(local_val, &dts)
380+
pandas_datetime_to_datetimestruct(local_val, reso, &dts)
378381
res_val = get_period_ordinal(&dts, freq)
379382

380383
# Analogous to: result[i] = res_val

pandas/core/arrays/period.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
astype_overflowsafe,
2323
delta_to_nanoseconds,
2424
dt64arr_to_periodarr as c_dt64arr_to_periodarr,
25+
get_unit_from_dtype,
2526
iNaT,
2627
parsing,
2728
period as libperiod,
@@ -1024,7 +1025,7 @@ def dt64arr_to_periodarr(data, freq, tz=None):
10241025
used.
10251026
10261027
"""
1027-
if data.dtype != np.dtype("M8[ns]"):
1028+
if not isinstance(data.dtype, np.dtype) or data.dtype.kind != "M":
10281029
raise ValueError(f"Wrong dtype: {data.dtype}")
10291030

10301031
if freq is None:
@@ -1036,9 +1037,10 @@ def dt64arr_to_periodarr(data, freq, tz=None):
10361037
elif isinstance(data, (ABCIndex, ABCSeries)):
10371038
data = data._values
10381039

1040+
reso = get_unit_from_dtype(data.dtype)
10391041
freq = Period._maybe_convert_freq(freq)
10401042
base = freq._period_dtype_code
1041-
return c_dt64arr_to_periodarr(data.view("i8"), base, tz), freq
1043+
return c_dt64arr_to_periodarr(data.view("i8"), base, tz, reso=reso), freq
10421044

10431045

10441046
def _get_ordinal_range(start, end, periods, freq, mult=1):

pandas/tests/arrays/test_datetimes.py

+30-11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,26 @@ def dtype(self, unit, tz_naive_fixture):
3737
else:
3838
return DatetimeTZDtype(unit=unit, tz=tz)
3939

40+
@pytest.fixture
41+
def dta_dti(self, unit, dtype):
42+
tz = getattr(dtype, "tz", None)
43+
44+
dti = pd.date_range("2016-01-01", periods=55, freq="D", tz=tz)
45+
if tz is None:
46+
arr = np.asarray(dti).astype(f"M8[{unit}]")
47+
else:
48+
arr = np.asarray(dti.tz_convert("UTC").tz_localize(None)).astype(
49+
f"M8[{unit}]"
50+
)
51+
52+
dta = DatetimeArray._simple_new(arr, dtype=dtype)
53+
return dta, dti
54+
55+
@pytest.fixture
56+
def dta(self, dta_dti):
57+
dta, dti = dta_dti
58+
return dta
59+
4060
def test_non_nano(self, unit, reso, dtype):
4161
arr = np.arange(5, dtype=np.int64).view(f"M8[{unit}]")
4262
dta = DatetimeArray._simple_new(arr, dtype=dtype)
@@ -52,17 +72,8 @@ def test_non_nano(self, unit, reso, dtype):
5272
@pytest.mark.parametrize(
5373
"field", DatetimeArray._field_ops + DatetimeArray._bool_ops
5474
)
55-
def test_fields(self, unit, reso, field, dtype):
56-
tz = getattr(dtype, "tz", None)
57-
dti = pd.date_range("2016-01-01", periods=55, freq="D", tz=tz)
58-
if tz is None:
59-
arr = np.asarray(dti).astype(f"M8[{unit}]")
60-
else:
61-
arr = np.asarray(dti.tz_convert("UTC").tz_localize(None)).astype(
62-
f"M8[{unit}]"
63-
)
64-
65-
dta = DatetimeArray._simple_new(arr, dtype=dtype)
75+
def test_fields(self, unit, reso, field, dtype, dta_dti):
76+
dta, dti = dta_dti
6677

6778
# FIXME: assert (dti == dta).all()
6879

@@ -107,6 +118,14 @@ def test_std_non_nano(self, unit):
107118
assert res._reso == dta._reso
108119
assert res == dti.std().floor(unit)
109120

121+
@pytest.mark.filterwarnings("ignore:Converting to PeriodArray.*:UserWarning")
122+
def test_to_period(self, dta_dti):
123+
dta, dti = dta_dti
124+
result = dta.to_period("D")
125+
expected = dti._data.to_period("D")
126+
127+
tm.assert_extension_array_equal(result, expected)
128+
110129

111130
class TestDatetimeArrayComparisons:
112131
# TODO: merge this into tests/arithmetic/test_datetime64 once it is

pandas/tests/indexes/period/test_constructors.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,10 @@ def test_constructor_datetime64arr(self):
183183
vals = np.arange(100000, 100000 + 10000, 100, dtype=np.int64)
184184
vals = vals.view(np.dtype("M8[us]"))
185185

186-
msg = r"Wrong dtype: datetime64\[us\]"
187-
with pytest.raises(ValueError, match=msg):
188-
PeriodIndex(vals, freq="D")
186+
pi = PeriodIndex(vals, freq="D")
187+
188+
expected = PeriodIndex(vals.astype("M8[ns]"), freq="D")
189+
tm.assert_index_equal(pi, expected)
189190

190191
@pytest.mark.parametrize("box", [None, "series", "index"])
191192
def test_constructor_datetime64arr_ok(self, box):

setup.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,11 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
551551
"depends": tseries_depends,
552552
"sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"],
553553
},
554-
"_libs.tslibs.vectorized": {"pyxfile": "_libs/tslibs/vectorized"},
554+
"_libs.tslibs.vectorized": {
555+
"pyxfile": "_libs/tslibs/vectorized",
556+
"depends": tseries_depends,
557+
"sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"],
558+
},
555559
"_libs.testing": {"pyxfile": "_libs/testing"},
556560
"_libs.window.aggregations": {
557561
"pyxfile": "_libs/window/aggregations",

0 commit comments

Comments
 (0)