diff --git a/pandas/_libs/tslibs/ccalendar.pxd b/pandas/_libs/tslibs/ccalendar.pxd index 41cc477413607..4eb5188b8a04b 100644 --- a/pandas/_libs/tslibs/ccalendar.pxd +++ b/pandas/_libs/tslibs/ccalendar.pxd @@ -10,6 +10,8 @@ cpdef int32_t get_days_in_month(int year, Py_ssize_t month) nogil cpdef int32_t get_week_of_year(int year, int month, int day) nogil cpdef iso_calendar_t get_iso_calendar(int year, int month, int day) nogil cpdef int32_t get_day_of_year(int year, int month, int day) nogil +cpdef int get_lastbday(int year, int month) nogil +cpdef int get_firstbday(int year, int month) nogil cdef int64_t DAY_NANOS cdef int64_t HOUR_NANOS diff --git a/pandas/_libs/tslibs/ccalendar.pyx b/pandas/_libs/tslibs/ccalendar.pyx index de8fd3911e946..00cecd25e5225 100644 --- a/pandas/_libs/tslibs/ccalendar.pyx +++ b/pandas/_libs/tslibs/ccalendar.pyx @@ -241,3 +241,52 @@ cpdef int32_t get_day_of_year(int year, int month, int day) nogil: day_of_year = mo_off + day return day_of_year + + +# --------------------------------------------------------------------- +# Business Helpers + +cpdef int get_lastbday(int year, int month) nogil: + """ + Find the last day of the month that is a business day. + + Parameters + ---------- + year : int + month : int + + Returns + ------- + last_bday : int + """ + cdef: + int wkday, days_in_month + + wkday = dayofweek(year, month, 1) + days_in_month = get_days_in_month(year, month) + return days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0) + + +cpdef int get_firstbday(int year, int month) nogil: + """ + Find the first day of the month that is a business day. + + Parameters + ---------- + year : int + month : int + + Returns + ------- + first_bday : int + """ + cdef: + int first, wkday + + wkday = dayofweek(year, month, 1) + first = 1 + if wkday == 5: # on Saturday + first = 3 + elif wkday == 6: # on Sunday + first = 2 + return first diff --git a/pandas/_libs/tslibs/fields.pyx b/pandas/_libs/tslibs/fields.pyx index 5ea7c0b6c5d02..03e4188fd06ef 100644 --- a/pandas/_libs/tslibs/fields.pyx +++ b/pandas/_libs/tslibs/fields.pyx @@ -19,6 +19,8 @@ from pandas._libs.tslibs.ccalendar cimport ( get_days_in_month, is_leapyear, dayofweek, get_week_of_year, get_day_of_year, get_iso_calendar, iso_calendar_t, month_offset, + get_firstbday, + get_lastbday, ) from pandas._libs.tslibs.np_datetime cimport ( npy_datetimestruct, pandas_timedeltastruct, dt64_to_dtstruct, @@ -137,9 +139,7 @@ def get_start_end_field(const int64_t[:] dtindex, str field, int end_month = 12 int start_month = 1 ndarray[int8_t] out - bint isleap npy_datetimestruct dts - int mo_off, dom, doy, dow, ldom out = np.zeros(count, dtype='int8') @@ -172,10 +172,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - dow = dayofweek(dts.year, dts.month, dts.day) - if (dom == 1 and dow < 5) or (dom <= 3 and dow == 0): + if dts.day == get_firstbday(dts.year, dts.month): out[i] = 1 else: @@ -185,9 +183,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - if dom == 1: + if dts.day == 1: out[i] = 1 elif field == 'is_month_end': @@ -198,15 +195,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - mo_off = month_offset[isleap * 13 + dts.month - 1] - dom = dts.day - doy = mo_off + dom - ldom = month_offset[isleap * 13 + dts.month] - dow = dayofweek(dts.year, dts.month, dts.day) - - if (ldom == doy and dow < 5) or ( - dow == 4 and (ldom - doy <= 2)): + + if dts.day == get_lastbday(dts.year, dts.month): out[i] = 1 else: @@ -216,13 +206,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - mo_off = month_offset[isleap * 13 + dts.month - 1] - dom = dts.day - doy = mo_off + dom - ldom = month_offset[isleap * 13 + dts.month] - if ldom == doy: + if dts.day == get_days_in_month(dts.year, dts.month): out[i] = 1 elif field == 'is_quarter_start': @@ -233,11 +218,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - dow = dayofweek(dts.year, dts.month, dts.day) if ((dts.month - start_month) % 3 == 0) and ( - (dom == 1 and dow < 5) or (dom <= 3 and dow == 0)): + dts.day == get_firstbday(dts.year, dts.month)): out[i] = 1 else: @@ -247,9 +230,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - if ((dts.month - start_month) % 3 == 0) and dom == 1: + if ((dts.month - start_month) % 3 == 0) and dts.day == 1: out[i] = 1 elif field == 'is_quarter_end': @@ -260,16 +242,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - mo_off = month_offset[isleap * 13 + dts.month - 1] - dom = dts.day - doy = mo_off + dom - ldom = month_offset[isleap * 13 + dts.month] - dow = dayofweek(dts.year, dts.month, dts.day) if ((dts.month - end_month) % 3 == 0) and ( - (ldom == doy and dow < 5) or ( - dow == 4 and (ldom - doy <= 2))): + dts.day == get_lastbday(dts.year, dts.month)): out[i] = 1 else: @@ -279,13 +254,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - mo_off = month_offset[isleap * 13 + dts.month - 1] - dom = dts.day - doy = mo_off + dom - ldom = month_offset[isleap * 13 + dts.month] - if ((dts.month - end_month) % 3 == 0) and (ldom == doy): + if ((dts.month - end_month) % 3 == 0) and ( + dts.day == get_days_in_month(dts.year, dts.month)): out[i] = 1 elif field == 'is_year_start': @@ -296,11 +267,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - dow = dayofweek(dts.year, dts.month, dts.day) if (dts.month == start_month) and ( - (dom == 1 and dow < 5) or (dom <= 3 and dow == 0)): + dts.day == get_firstbday(dts.year, dts.month)): out[i] = 1 else: @@ -310,9 +279,8 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - dom = dts.day - if (dts.month == start_month) and dom == 1: + if (dts.month == start_month) and dts.day == 1: out[i] = 1 elif field == 'is_year_end': @@ -323,16 +291,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - dom = dts.day - mo_off = month_offset[isleap * 13 + dts.month - 1] - doy = mo_off + dom - dow = dayofweek(dts.year, dts.month, dts.day) - ldom = month_offset[isleap * 13 + dts.month] if (dts.month == end_month) and ( - (ldom == doy and dow < 5) or ( - dow == 4 and (ldom - doy <= 2))): + dts.day == get_lastbday(dts.year, dts.month)): out[i] = 1 else: @@ -342,13 +303,9 @@ def get_start_end_field(const int64_t[:] dtindex, str field, continue dt64_to_dtstruct(dtindex[i], &dts) - isleap = is_leapyear(dts.year) - mo_off = month_offset[isleap * 13 + dts.month - 1] - dom = dts.day - doy = mo_off + dom - ldom = month_offset[isleap * 13 + dts.month] - if (dts.month == end_month) and (ldom == doy): + if (dts.month == end_month) and ( + dts.day == get_days_in_month(dts.year, dts.month)): out[i] = 1 else: diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 0f9280ae92d39..4429ff083f350 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -34,7 +34,13 @@ from pandas._libs.tslibs.util cimport ( from pandas._libs.tslibs.ccalendar import ( MONTH_ALIASES, MONTH_TO_CAL_NUM, weekday_to_int, int_to_weekday, ) -from pandas._libs.tslibs.ccalendar cimport DAY_NANOS, get_days_in_month, dayofweek +from pandas._libs.tslibs.ccalendar cimport ( + DAY_NANOS, + dayofweek, + get_days_in_month, + get_firstbday, + get_lastbday, +) from pandas._libs.tslibs.conversion cimport ( convert_datetime_to_tsobject, localize_pydatetime, @@ -177,51 +183,6 @@ cdef _wrap_timedelta_result(result): # --------------------------------------------------------------------- # Business Helpers -cpdef int get_lastbday(int year, int month) nogil: - """ - Find the last day of the month that is a business day. - - Parameters - ---------- - year : int - month : int - - Returns - ------- - last_bday : int - """ - cdef: - int wkday, days_in_month - - wkday = dayofweek(year, month, 1) - days_in_month = get_days_in_month(year, month) - return days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0) - - -cpdef int get_firstbday(int year, int month) nogil: - """ - Find the first day of the month that is a business day. - - Parameters - ---------- - year : int - month : int - - Returns - ------- - first_bday : int - """ - cdef: - int first, wkday - - wkday = dayofweek(year, month, 1) - first = 1 - if wkday == 5: # on Saturday - first = 3 - elif wkday == 6: # on Sunday - first = 2 - return first - cdef _get_calendar(weekmask, holidays, calendar): """Generate busdaycalendar""" diff --git a/pandas/tests/tslibs/test_liboffsets.py b/pandas/tests/tslibs/test_liboffsets.py index 206a604788c7e..6a514d2cc8713 100644 --- a/pandas/tests/tslibs/test_liboffsets.py +++ b/pandas/tests/tslibs/test_liboffsets.py @@ -5,6 +5,7 @@ import pytest +from pandas._libs.tslibs.ccalendar import get_firstbday, get_lastbday import pandas._libs.tslibs.offsets as liboffsets from pandas._libs.tslibs.offsets import roll_qtrday @@ -25,7 +26,7 @@ def day_opt(request): ) def test_get_last_bday(dt, exp_week_day, exp_last_day): assert dt.weekday() == exp_week_day - assert liboffsets.get_lastbday(dt.year, dt.month) == exp_last_day + assert get_lastbday(dt.year, dt.month) == exp_last_day @pytest.mark.parametrize( @@ -37,7 +38,7 @@ def test_get_last_bday(dt, exp_week_day, exp_last_day): ) def test_get_first_bday(dt, exp_week_day, exp_first_day): assert dt.weekday() == exp_week_day - assert liboffsets.get_firstbday(dt.year, dt.month) == exp_first_day + assert get_firstbday(dt.year, dt.month) == exp_first_day @pytest.mark.parametrize(