Skip to content

Commit 2ad0c46

Browse files
committed
ENH: DTA to_pydatetime, time, timetz, date, iter support non-nano
1 parent 05f85a4 commit 2ad0c46

File tree

5 files changed

+74
-22
lines changed

5 files changed

+74
-22
lines changed

pandas/_libs/tslibs/vectorized.pyi

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def ints_to_pydatetime(
3535
freq: BaseOffset | None = ...,
3636
fold: bool = ...,
3737
box: str = ...,
38+
reso: int = ..., # NPY_DATETIMEUNIT
3839
) -> npt.NDArray[np.object_]: ...
3940
def tz_convert_from_utc(
4041
stamps: npt.NDArray[np.int64],

pandas/_libs/tslibs/vectorized.pyx

+10-4
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
@@ -99,7 +100,8 @@ def ints_to_pydatetime(
99100
tzinfo tz=None,
100101
BaseOffset freq=None,
101102
bint fold=False,
102-
str box="datetime"
103+
str box="datetime",
104+
NPY_DATETIMEUNIT reso=NPY_FR_ns,
103105
) -> np.ndarray:
104106
# stamps is int64, arbitrary ndim
105107
"""
@@ -125,12 +127,14 @@ def ints_to_pydatetime(
125127
* If time, convert to datetime.time
126128
* If Timestamp, convert to pandas.Timestamp
127129

130+
reso : NPY_DATETIMEUNIT, default NPY_FR_ns
131+
128132
Returns
129133
-------
130134
ndarray[object] of type specified by box
131135
"""
132136
cdef:
133-
Localizer info = Localizer(tz, reso=NPY_FR_ns)
137+
Localizer info = Localizer(tz, reso=reso)
134138
int64_t utc_val, local_val
135139
Py_ssize_t i, n = stamps.size
136140
Py_ssize_t pos = -1 # unused, avoid not-initialized warning
@@ -178,10 +182,12 @@ def ints_to_pydatetime(
178182
# find right representation of dst etc in pytz timezone
179183
new_tz = tz._tzinfos[tz._transition_info[pos]]
180184

181-
dt64_to_dtstruct(local_val, &dts)
185+
pandas_datetime_to_datetimestruct(local_val, reso, &dts)
182186

183187
if use_ts:
184-
res_val = create_timestamp_from_ts(utc_val, dts, new_tz, freq, fold)
188+
res_val = create_timestamp_from_ts(
189+
utc_val, dts, new_tz, freq, fold, reso=reso
190+
)
185191
elif use_pydt:
186192
res_val = datetime(
187193
dts.year, dts.month, dts.day, dts.hour, dts.min, dts.sec, dts.us,

pandas/core/arrays/datetimelike.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -426,15 +426,16 @@ def astype(self, dtype, copy: bool = True):
426426
if self.dtype.kind == "M":
427427
# *much* faster than self._box_values
428428
# for e.g. test_get_loc_tuple_monotonic_above_size_cutoff
429-
i8data = self.asi8.ravel()
429+
i8data = self.asi8
430430
converted = ints_to_pydatetime(
431431
i8data,
432432
# error: "DatetimeLikeArrayMixin" has no attribute "tz"
433433
tz=self.tz, # type: ignore[attr-defined]
434434
freq=self.freq,
435435
box="timestamp",
436+
reso=self._reso,
436437
)
437-
return converted.reshape(self.shape)
438+
return converted
438439

439440
elif self.dtype.kind == "m":
440441
return ints_to_pytimedelta(self._ndarray, box=True)

pandas/core/arrays/datetimes.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,11 @@ def __iter__(self):
650650
start_i = i * chunksize
651651
end_i = min((i + 1) * chunksize, length)
652652
converted = ints_to_pydatetime(
653-
data[start_i:end_i], tz=self.tz, freq=self.freq, box="timestamp"
653+
data[start_i:end_i],
654+
tz=self.tz,
655+
freq=self.freq,
656+
box="timestamp",
657+
reso=self._reso,
654658
)
655659
yield from converted
656660

@@ -1041,7 +1045,7 @@ def to_pydatetime(self) -> npt.NDArray[np.object_]:
10411045
-------
10421046
datetimes : ndarray[object]
10431047
"""
1044-
return ints_to_pydatetime(self.asi8, tz=self.tz)
1048+
return ints_to_pydatetime(self.asi8, tz=self.tz, reso=self._reso)
10451049

10461050
def normalize(self) -> DatetimeArray:
10471051
"""
@@ -1295,7 +1299,7 @@ def time(self) -> npt.NDArray[np.object_]:
12951299
# keeping their timezone and not using UTC
12961300
timestamps = self._local_timestamps()
12971301

1298-
return ints_to_pydatetime(timestamps, box="time")
1302+
return ints_to_pydatetime(timestamps, box="time", reso=self._reso)
12991303

13001304
@property
13011305
def timetz(self) -> npt.NDArray[np.object_]:
@@ -1305,7 +1309,7 @@ def timetz(self) -> npt.NDArray[np.object_]:
13051309
13061310
The time part of the Timestamps.
13071311
"""
1308-
return ints_to_pydatetime(self.asi8, self.tz, box="time")
1312+
return ints_to_pydatetime(self.asi8, self.tz, box="time", reso=self._reso)
13091313

13101314
@property
13111315
def date(self) -> npt.NDArray[np.object_]:
@@ -1320,7 +1324,7 @@ def date(self) -> npt.NDArray[np.object_]:
13201324
# keeping their timezone and not using UTC
13211325
timestamps = self._local_timestamps()
13221326

1323-
return ints_to_pydatetime(timestamps, box="date")
1327+
return ints_to_pydatetime(timestamps, box="date", reso=self._reso)
13241328

13251329
def isocalendar(self) -> DataFrame:
13261330
"""

pandas/tests/arrays/test_datetimes.py

+51-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

@@ -85,6 +96,35 @@ def test_normalize(self, unit):
8596
res = dta.normalize()
8697
tm.assert_extension_array_equal(res, expected)
8798

99+
def test_iter(self, dta):
100+
res = next(iter(dta))
101+
expected = dta[0]
102+
103+
assert type(res) is pd.Timestamp
104+
assert res.value == expected.value
105+
assert res._reso == expected._reso
106+
assert res == expected
107+
108+
def test_astype_object(self, dta):
109+
result = dta.astype(object)
110+
assert all(x._reso == dta._reso for x in result)
111+
assert all(x == y for x, y in zip(result, dta))
112+
113+
def test_to_pydatetime(self, dta_dti):
114+
dta, dti = dta_dti
115+
116+
result = dta.to_pydatetime()
117+
expected = dti.to_pydatetime()
118+
tm.assert_numpy_array_equal(result, expected)
119+
120+
@pytest.mark.parametrize("meth", ["time", "timetz", "date"])
121+
def test_time_date(self, dta_dti, meth):
122+
dta, dti = dta_dti
123+
124+
result = getattr(dta, meth)
125+
expected = getattr(dti, meth)
126+
tm.assert_numpy_array_equal(result, expected)
127+
88128

89129
class TestDatetimeArrayComparisons:
90130
# TODO: merge this into tests/arithmetic/test_datetime64 once it is

0 commit comments

Comments
 (0)