Skip to content

Commit 422e92a

Browse files
authored
ENH: fields.get_start_end_field support non-nano (#46902)
* ENH: fields.get_start_end_field support non-nano * fix non-mac builds
1 parent 11462d6 commit 422e92a

File tree

8 files changed

+58
-15
lines changed

8 files changed

+58
-15
lines changed

asv_bench/benchmarks/tslibs/fields.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,9 @@ class TimeGetStartEndField:
6666

6767
def setup(self, size, side, period, freqstr, month_kw):
6868
arr = np.random.randint(0, 10, size=size, dtype="i8")
69-
self.i8data = arr
69+
self.dt64data = arr.view("M8[ns]")
7070

7171
self.attrname = f"is_{period}_{side}"
7272

7373
def time_get_start_end_field(self, size, side, period, freqstr, month_kw):
74-
get_start_end_field(self.i8data, self.attrname, freqstr, month_kw=month_kw)
74+
get_start_end_field(self.dt64data, self.attrname, freqstr, month_kw=month_kw)

pandas/_libs/tslibs/fields.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def get_date_name_field(
1212
locale: str | None = ...,
1313
) -> npt.NDArray[np.object_]: ...
1414
def get_start_end_field(
15-
dtindex: npt.NDArray[np.int64], # const int64_t[:]
15+
dt64values: npt.NDArray[np.datetime64],
1616
field: str,
1717
freqstr: str | None = ...,
1818
month_kw: int = ...,

pandas/_libs/tslibs/fields.pyx

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ objects and arrays
55
from locale import LC_TIME
66

77
from _strptime import LocaleTime
8+
89
cimport cython
910
from cython cimport Py_ssize_t
11+
1012
import numpy as np
1113

1214
cimport numpy as cnp
@@ -41,8 +43,11 @@ from pandas._libs.tslibs.ccalendar cimport (
4143
)
4244
from pandas._libs.tslibs.nattype cimport NPY_NAT
4345
from pandas._libs.tslibs.np_datetime cimport (
46+
NPY_DATETIMEUNIT,
4447
dt64_to_dtstruct,
48+
get_unit_from_dtype,
4549
npy_datetimestruct,
50+
pandas_datetime_to_datetimestruct,
4651
pandas_timedeltastruct,
4752
td64_to_tdstruct,
4853
)
@@ -196,22 +201,35 @@ cdef inline bint _is_on_month(int month, int compare_month, int modby) nogil:
196201

197202
@cython.wraparound(False)
198203
@cython.boundscheck(False)
199-
def get_start_end_field(const int64_t[:] dtindex, str field,
204+
def get_start_end_field(ndarray dt64values, str field,
200205
str freqstr=None, int month_kw=12):
201206
"""
202207
Given an int64-based datetime index return array of indicators
203208
of whether timestamps are at the start/end of the month/quarter/year
204209
(defined by frequency).
210+
211+
Parameters
212+
----------
213+
dt64values : ndarray[datetime64], any resolution
214+
field : str
215+
frestr : str or None, default None
216+
month_kw : int, default 12
217+
218+
Returns
219+
-------
220+
ndarray[bool]
205221
"""
206222
cdef:
207223
Py_ssize_t i
208-
int count = len(dtindex)
224+
int count = dt64values.size
209225
bint is_business = 0
210226
int end_month = 12
211227
int start_month = 1
212228
ndarray[int8_t] out
213229
npy_datetimestruct dts
214230
int compare_month, modby
231+
ndarray dtindex = dt64values.view("i8")
232+
NPY_DATETIMEUNIT reso = get_unit_from_dtype(dt64values.dtype)
215233

216234
out = np.zeros(count, dtype='int8')
217235

@@ -251,7 +269,7 @@ def get_start_end_field(const int64_t[:] dtindex, str field,
251269
out[i] = 0
252270
continue
253271

254-
dt64_to_dtstruct(dtindex[i], &dts)
272+
pandas_datetime_to_datetimestruct(dtindex[i], reso, &dts)
255273

256274
if _is_on_month(dts.month, compare_month, modby) and (
257275
dts.day == get_firstbday(dts.year, dts.month)):
@@ -263,7 +281,7 @@ def get_start_end_field(const int64_t[:] dtindex, str field,
263281
out[i] = 0
264282
continue
265283

266-
dt64_to_dtstruct(dtindex[i], &dts)
284+
pandas_datetime_to_datetimestruct(dtindex[i], reso, &dts)
267285

268286
if _is_on_month(dts.month, compare_month, modby) and dts.day == 1:
269287
out[i] = 1
@@ -275,7 +293,7 @@ def get_start_end_field(const int64_t[:] dtindex, str field,
275293
out[i] = 0
276294
continue
277295

278-
dt64_to_dtstruct(dtindex[i], &dts)
296+
pandas_datetime_to_datetimestruct(dtindex[i], reso, &dts)
279297

280298
if _is_on_month(dts.month, compare_month, modby) and (
281299
dts.day == get_lastbday(dts.year, dts.month)):
@@ -287,7 +305,7 @@ def get_start_end_field(const int64_t[:] dtindex, str field,
287305
out[i] = 0
288306
continue
289307

290-
dt64_to_dtstruct(dtindex[i], &dts)
308+
pandas_datetime_to_datetimestruct(dtindex[i], reso, &dts)
291309

292310
if _is_on_month(dts.month, compare_month, modby) and (
293311
dts.day == get_days_in_month(dts.year, dts.month)):

pandas/_libs/tslibs/timestamps.pyx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,9 +487,7 @@ cdef class _Timestamp(ABCTimestamp):
487487
dict kwds
488488
ndarray[uint8_t, cast=True] out
489489
int month_kw
490-
491-
if self._reso != NPY_FR_ns:
492-
raise NotImplementedError(self._reso)
490+
str unit
493491

494492
if freq:
495493
kwds = freq.kwds
@@ -500,7 +498,9 @@ cdef class _Timestamp(ABCTimestamp):
500498
freqstr = None
501499

502500
val = self._maybe_convert_value_to_local()
503-
out = get_start_end_field(np.array([val], dtype=np.int64),
501+
502+
unit = npy_unit_to_abbrev(self._reso)
503+
out = get_start_end_field(np.array([val], dtype=f"M8[{unit}]"),
504504
field, freqstr, month_kw)
505505
return out[0]
506506

pandas/core/arrays/datetimes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def f(self):
130130
month_kw = kwds.get("startingMonth", kwds.get("month", 12))
131131

132132
result = fields.get_start_end_field(
133-
values, field, self.freqstr, month_kw
133+
values.view(self._ndarray.dtype), field, self.freqstr, month_kw
134134
)
135135
else:
136136
result = fields.get_date_field(values, field)

pandas/tests/scalar/timestamp/test_timestamp.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,27 @@ def test_non_nano_fields(self, dt64, ts):
732732
assert ts.weekday() == alt.weekday()
733733
assert ts.isoweekday() == alt.isoweekday()
734734

735+
def test_start_end_fields(self, ts):
736+
assert ts.is_year_start
737+
assert ts.is_quarter_start
738+
assert ts.is_month_start
739+
assert not ts.is_year_end
740+
assert not ts.is_month_end
741+
assert not ts.is_month_end
742+
743+
freq = offsets.BDay()
744+
ts._set_freq(freq)
745+
746+
# 2016-01-01 is a Friday, so is year/quarter/month start with this freq
747+
msg = "Timestamp.freq is deprecated"
748+
with tm.assert_produces_warning(FutureWarning, match=msg):
749+
assert ts.is_year_start
750+
assert ts.is_quarter_start
751+
assert ts.is_month_start
752+
assert not ts.is_year_end
753+
assert not ts.is_month_end
754+
assert not ts.is_month_end
755+
735756
def test_repr(self, dt64, ts):
736757
alt = Timestamp(dt64)
737758

pandas/tests/tslibs/test_fields.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ def test_get_date_field_readonly(dtindex):
2828

2929

3030
def test_get_start_end_field_readonly(dtindex):
31-
result = fields.get_start_end_field(dtindex, "is_month_start", None)
31+
dt64values = dtindex.view("M8[ns]")
32+
dt64values.flags.writeable = False
33+
34+
result = fields.get_start_end_field(dt64values, "is_month_start", None)
3235
expected = np.array([True, False, False, False, False], dtype=np.bool_)
3336
tm.assert_numpy_array_equal(result, expected)
3437

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,7 @@ def srcpath(name=None, suffix=".pyx", subdir="src"):
506506
"_libs.tslibs.fields": {
507507
"pyxfile": "_libs/tslibs/fields",
508508
"depends": tseries_depends,
509+
"sources": ["pandas/_libs/tslibs/src/datetime/np_datetime.c"],
509510
},
510511
"_libs.tslibs.nattype": {"pyxfile": "_libs/tslibs/nattype"},
511512
"_libs.tslibs.np_datetime": {

0 commit comments

Comments
 (0)