Skip to content

Commit 94a9f7d

Browse files
authored
REF: avoid DTA/PA methods in SemiMonthOffset.apply_index (#34783)
1 parent 18ab444 commit 94a9f7d

File tree

1 file changed

+49
-108
lines changed

1 file changed

+49
-108
lines changed

pandas/_libs/tslibs/offsets.pyx

+49-108
Original file line numberDiff line numberDiff line change
@@ -2269,56 +2269,62 @@ cdef class SemiMonthOffset(SingleConstructorOffset):
22692269
raise NotImplementedError(self)
22702270

22712271
@apply_index_wraps
2272+
@cython.wraparound(False)
2273+
@cython.boundscheck(False)
22722274
def apply_index(self, dtindex):
2273-
# determine how many days away from the 1st of the month we are
2274-
2275-
dti = dtindex
2276-
i8other = dtindex.asi8
2277-
days_from_start = dtindex.to_perioddelta("M").asi8
2278-
delta = Timedelta(days=self.day_of_month - 1).value
2279-
2280-
# get boolean array for each element before the day_of_month
2281-
before_day_of_month = days_from_start < delta
2282-
2283-
# get boolean array for each element after the day_of_month
2284-
after_day_of_month = days_from_start > delta
2285-
2286-
# determine the correct n for each date in dtindex
2287-
roll = self._get_roll(i8other, before_day_of_month, after_day_of_month)
2288-
2289-
# isolate the time since it will be striped away one the next line
2290-
time = (i8other % DAY_NANOS).view("timedelta64[ns]")
2291-
2292-
# apply the correct number of months
2293-
2294-
# integer-array addition on PeriodIndex is deprecated,
2295-
# so we use _addsub_int_array directly
2296-
asper = dtindex.to_period("M")
2275+
cdef:
2276+
int64_t[:] i8other = dtindex.view("i8")
2277+
Py_ssize_t i, count = len(i8other)
2278+
int64_t val
2279+
int64_t[:] out = np.empty(count, dtype="i8")
2280+
npy_datetimestruct dts
2281+
int months, to_day, nadj, n = self.n
2282+
int days_in_month, day, anchor_dom = self.day_of_month
2283+
bint is_start = isinstance(self, SemiMonthBegin)
22972284

2298-
shifted = asper._addsub_int_array(roll // 2, operator.add)
2299-
dtindex = type(dti)(shifted.to_timestamp())
2300-
dt64other = np.asarray(dtindex)
2285+
with nogil:
2286+
for i in range(count):
2287+
val = i8other[i]
2288+
if val == NPY_NAT:
2289+
out[i] = NPY_NAT
2290+
continue
23012291

2302-
# apply the correct day
2303-
dt64result = self._apply_index_days(dt64other, roll)
2292+
dt64_to_dtstruct(val, &dts)
2293+
day = dts.day
2294+
2295+
# Adjust so that we are always looking at self.day_of_month,
2296+
# incrementing/decrementing n if necessary.
2297+
nadj = roll_convention(day, n, anchor_dom)
2298+
2299+
days_in_month = get_days_in_month(dts.year, dts.month)
2300+
# For SemiMonthBegin on other.day == 1 and
2301+
# SemiMonthEnd on other.day == days_in_month,
2302+
# shifting `other` to `self.day_of_month` _always_ requires
2303+
# incrementing/decrementing `n`, regardless of whether it is
2304+
# initially positive.
2305+
if is_start and (n <= 0 and day == 1):
2306+
nadj -= 1
2307+
elif (not is_start) and (n > 0 and day == days_in_month):
2308+
nadj += 1
2309+
2310+
if is_start:
2311+
# See also: SemiMonthBegin._apply
2312+
months = nadj // 2 + nadj % 2
2313+
to_day = 1 if nadj % 2 else anchor_dom
23042314

2305-
return dt64result + time
2315+
else:
2316+
# See also: SemiMonthEnd._apply
2317+
months = nadj // 2
2318+
to_day = 31 if nadj % 2 else anchor_dom
23062319

2307-
def _get_roll(self, i8other, before_day_of_month, after_day_of_month):
2308-
"""
2309-
Return an array with the correct n for each date in dtindex.
2320+
dts.year = year_add_months(dts, months)
2321+
dts.month = month_add_months(dts, months)
2322+
days_in_month = get_days_in_month(dts.year, dts.month)
2323+
dts.day = min(to_day, days_in_month)
23102324

2311-
The roll array is based on the fact that dtindex gets rolled back to
2312-
the first day of the month.
2313-
"""
2314-
# before_day_of_month and after_day_of_month are ndarray[bool]
2315-
raise NotImplementedError
2325+
out[i] = dtstruct_to_dt64(&dts)
23162326

2317-
def _apply_index_days(self, dt64other, roll):
2318-
"""
2319-
Apply the correct day for each date in dt64other.
2320-
"""
2321-
raise NotImplementedError
2327+
return out.base
23222328

23232329

23242330
cdef class SemiMonthEnd(SemiMonthOffset):
@@ -2347,39 +2353,6 @@ cdef class SemiMonthEnd(SemiMonthOffset):
23472353
day = 31 if n % 2 else self.day_of_month
23482354
return shift_month(other, months, day)
23492355

2350-
def _get_roll(self, i8other, before_day_of_month, after_day_of_month):
2351-
# before_day_of_month and after_day_of_month are ndarray[bool]
2352-
n = self.n
2353-
is_month_end = get_start_end_field(i8other, "is_month_end")
2354-
if n > 0:
2355-
roll_end = np.where(is_month_end, 1, 0)
2356-
roll_before = np.where(before_day_of_month, n, n + 1)
2357-
roll = roll_end + roll_before
2358-
elif n == 0:
2359-
roll_after = np.where(after_day_of_month, 2, 0)
2360-
roll_before = np.where(~after_day_of_month, 1, 0)
2361-
roll = roll_before + roll_after
2362-
else:
2363-
roll = np.where(after_day_of_month, n + 2, n + 1)
2364-
return roll
2365-
2366-
def _apply_index_days(self, dt64other, roll):
2367-
"""
2368-
Add days portion of offset to dt64other.
2369-
2370-
Parameters
2371-
----------
2372-
dt64other : ndarray[datetime64[ns]]
2373-
roll : ndarray[int64_t]
2374-
2375-
Returns
2376-
-------
2377-
ndarray[datetime64[ns]]
2378-
"""
2379-
nanos = (roll % 2) * Timedelta(days=self.day_of_month).value
2380-
dt64other += nanos.astype("timedelta64[ns]")
2381-
return dt64other + Timedelta(days=-1)
2382-
23832356

23842357
cdef class SemiMonthBegin(SemiMonthOffset):
23852358
"""
@@ -2405,38 +2378,6 @@ cdef class SemiMonthBegin(SemiMonthOffset):
24052378
day = 1 if n % 2 else self.day_of_month
24062379
return shift_month(other, months, day)
24072380

2408-
def _get_roll(self, i8other, before_day_of_month, after_day_of_month):
2409-
# before_day_of_month and after_day_of_month are ndarray[bool]
2410-
n = self.n
2411-
is_month_start = get_start_end_field(i8other, "is_month_start")
2412-
if n > 0:
2413-
roll = np.where(before_day_of_month, n, n + 1)
2414-
elif n == 0:
2415-
roll_start = np.where(is_month_start, 0, 1)
2416-
roll_after = np.where(after_day_of_month, 1, 0)
2417-
roll = roll_start + roll_after
2418-
else:
2419-
roll_after = np.where(after_day_of_month, n + 2, n + 1)
2420-
roll_start = np.where(is_month_start, -1, 0)
2421-
roll = roll_after + roll_start
2422-
return roll
2423-
2424-
def _apply_index_days(self, dt64other, roll):
2425-
"""
2426-
Add days portion of offset to dt64other.
2427-
2428-
Parameters
2429-
----------
2430-
dt64other : ndarray[datetime64[ns]]
2431-
roll : ndarray[int64_t]
2432-
2433-
Returns
2434-
-------
2435-
ndarray[datetime64[ns]]
2436-
"""
2437-
nanos = (roll % 2) * Timedelta(days=self.day_of_month - 1).value
2438-
return dt64other + nanos.astype("timedelta64[ns]")
2439-
24402381

24412382
# ---------------------------------------------------------------------
24422383
# Week-Based Offset Classes

0 commit comments

Comments
 (0)