Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v3.0.4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Bug fixes

- Bug in :meth:`DataFrame.iloc` silently ignoring the assignment when setting values with an unordered or duplicated column indexer on a :class:`DataFrame` whose values are referenced by another object (:issue:`65446`)
- Bug in :meth:`Series.str.__getitem__` raising ``AttributeError`` when underlying array is :class:`ArrowExtensionArray` (:issue:`65112`)
- Bug in arithmetic adding or subtracting a non-tick :class:`DateOffset` (e.g. :class:`offsets.MonthEnd`, :class:`offsets.QuarterEnd`) to datetime data that could cause a segmentation fault when another thread was running concurrently, e.g. under ``pytest-xdist`` (:issue:`66031`)


.. ---------------------------------------------------------------------------
Expand Down
74 changes: 48 additions & 26 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,31 @@ from .timestamps cimport _Timestamp

from .timestamps import Timestamp

# day_opt enum: avoids touching Python str objects inside nogil loops

cdef enum _DayOpt:
DAY_OPT_START = 0
DAY_OPT_END = 1
DAY_OPT_BUSINESS_START = 2
DAY_OPT_BUSINESS_END = 3


cdef _DayOpt _str_to_day_opt(str day_opt) except? DAY_OPT_START:
if day_opt == "start":
return DAY_OPT_START
elif day_opt == "end":
return DAY_OPT_END
elif day_opt == "business_start":
return DAY_OPT_BUSINESS_START
elif day_opt == "business_end":
return DAY_OPT_BUSINESS_END
else:
raise ValueError(
"day must be 'start', 'end', 'business_start', or "
f"'business_end', got {day_opt}"
)


# ---------------------------------------------------------------------
# Misc Helpers

Expand Down Expand Up @@ -833,7 +858,7 @@ cdef class BaseOffset:
cdef:
npy_datetimestruct dts
pydate_to_dtstruct(other, &dts)
return get_day_of_month(&dts, self._day_opt)
return get_day_of_month(&dts, _str_to_day_opt(self._day_opt))

def is_on_offset(self, dt: datetime) -> bool:
"""
Expand Down Expand Up @@ -3175,7 +3200,7 @@ cdef class YearOffset(SingleConstructorOffset):
npy_datetimestruct dts
pydate_to_dtstruct(other, &dts)
dts.month = self._month
return get_day_of_month(&dts, self._day_opt)
return get_day_of_month(&dts, _str_to_day_opt(self._day_opt))

@apply_wraps
def _apply(self, other: datetime) -> datetime:
Expand Down Expand Up @@ -6468,6 +6493,7 @@ cdef ndarray shift_quarters(
int months_since, n
npy_datetimestruct dts
cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex)
_DayOpt day_opt_enum = _str_to_day_opt(day_opt)

with nogil:
for i in range(count):
Expand All @@ -6481,11 +6507,11 @@ cdef ndarray shift_quarters(
n = quarters

months_since = (dts.month - q1start_month) % modby
n = _roll_qtrday(&dts, n, months_since, day_opt)
n = _roll_qtrday(&dts, n, months_since, day_opt_enum)

dts.year = year_add_months(dts, modby * n - months_since)
dts.month = month_add_months(dts, modby * n - months_since)
dts.day = get_day_of_month(&dts, day_opt)
dts.day = get_day_of_month(&dts, day_opt_enum)

res_val = npy_datetimestruct_to_datetime(reso, &dts)

Expand Down Expand Up @@ -6521,6 +6547,7 @@ def shift_months(
ndarray out = cnp.PyArray_EMPTY(dtindex.ndim, dtindex.shape, cnp.NPY_INT64, 0)
int months_to_roll
int64_t val, res_val
_DayOpt day_opt_enum

cnp.broadcast mi = cnp.PyArray_MultiIterNew2(out, dtindex)

Expand Down Expand Up @@ -6553,6 +6580,7 @@ def shift_months(
cnp.PyArray_MultiIter_NEXT(mi)

else:
day_opt_enum = _str_to_day_opt(day_opt)
with nogil:
for i in range(count):

Expand All @@ -6565,11 +6593,13 @@ def shift_months(
pandas_datetime_to_datetimestruct(val, reso, &dts)
months_to_roll = months

months_to_roll = _roll_qtrday(&dts, months_to_roll, 0, day_opt)
months_to_roll = _roll_qtrday(
&dts, months_to_roll, 0, day_opt_enum
)

dts.year = year_add_months(dts, months_to_roll)
dts.month = month_add_months(dts, months_to_roll)
dts.day = get_day_of_month(&dts, day_opt)
dts.day = get_day_of_month(&dts, day_opt_enum)

res_val = npy_datetimestruct_to_datetime(reso, &dts)

Expand Down Expand Up @@ -6650,47 +6680,39 @@ def shift_month(stamp: datetime, months: int, day_opt: object = None) -> datetim
return stamp.replace(year=year, month=month, day=day)


cdef int get_day_of_month(npy_datetimestruct* dts, str day_opt) noexcept nogil:
cdef int get_day_of_month(npy_datetimestruct* dts, _DayOpt day_opt) noexcept nogil:
"""
Find the day in `other`'s month that satisfies a DateOffset's is_on_offset
policy, as described by the `day_opt` argument.

Parameters
----------
dts : npy_datetimestruct*
day_opt : {'start', 'end', 'business_start', 'business_end'}
'start': returns 1
'end': returns last day of the month
'business_start': returns the first business day of the month
'business_end': returns the last business day of the month
day_opt : _DayOpt enum value
DAY_OPT_START: returns 1
DAY_OPT_END: returns last day of the month
DAY_OPT_BUSINESS_START: returns the first business day of the month
DAY_OPT_BUSINESS_END: returns the last business day of the month

Returns
-------
day_of_month : int

Examples
-------
>>> other = datetime(2017, 11, 14)
>>> get_day_of_month(other, 'start')
1
>>> get_day_of_month(other, 'end')
30

Notes
-----
Caller is responsible for ensuring one of the four accepted day_opt values
is passed.
"""

if day_opt == "start":
if day_opt == DAY_OPT_START:
return 1
elif day_opt == "end":
elif day_opt == DAY_OPT_END:
return get_days_in_month(dts.year, dts.month)
elif day_opt == "business_start":
elif day_opt == DAY_OPT_BUSINESS_START:
# first business day of month
return get_firstbday(dts.year, dts.month)
else:
# i.e. day_opt == "business_end":
# i.e. day_opt == DAY_OPT_BUSINESS_END:
# last business day of month
return get_lastbday(dts.year, dts.month)

Expand Down Expand Up @@ -6758,13 +6780,13 @@ def roll_qtrday(other: datetime, n: int, month: int,
else:
months_since = other.month % modby - month % modby

return _roll_qtrday(&dts, n, months_since, day_opt)
return _roll_qtrday(&dts, n, months_since, _str_to_day_opt(day_opt))


cdef int _roll_qtrday(npy_datetimestruct* dts,
int n,
int months_since,
str day_opt) except? -1 nogil:
_DayOpt day_opt) noexcept nogil:
"""
See roll_qtrday.__doc__
"""
Expand Down
Loading