diff --git a/pandas/_libs/tslibs/ccalendar.pyx b/pandas/_libs/tslibs/ccalendar.pyx index 7d58b43e5d460..07c146c06b510 100644 --- a/pandas/_libs/tslibs/ccalendar.pyx +++ b/pandas/_libs/tslibs/ccalendar.pyx @@ -54,7 +54,7 @@ weekday_to_int = {int_to_weekday[key]: key for key in int_to_weekday} @cython.wraparound(False) @cython.boundscheck(False) -cpdef inline int32_t get_days_in_month(int year, Py_ssize_t month) nogil: +cpdef int32_t get_days_in_month(int year, Py_ssize_t month) nogil: """Return the number of days in the given month of the given year. Parameters diff --git a/pandas/_libs/tslibs/conversion.pyx b/pandas/_libs/tslibs/conversion.pyx index d91283deb9fc7..f79d6f370cc2d 100644 --- a/pandas/_libs/tslibs/conversion.pyx +++ b/pandas/_libs/tslibs/conversion.pyx @@ -75,7 +75,7 @@ cdef inline int64_t get_datetime64_nanos(object val) except? -1: @cython.boundscheck(False) @cython.wraparound(False) -def ensure_datetime64ns(ndarray arr, copy=True): +def ensure_datetime64ns(arr: ndarray, copy: bool=True): """ Ensure a np.datetime64 array has dtype specifically 'datetime64[ns]' @@ -122,7 +122,7 @@ def ensure_datetime64ns(ndarray arr, copy=True): return result -def ensure_timedelta64ns(ndarray arr, copy=True): +def ensure_timedelta64ns(arr: ndarray, copy: bint=True): """ Ensure a np.timedelta64 array has dtype specifically 'timedelta64[ns]' @@ -134,14 +134,14 @@ def ensure_timedelta64ns(ndarray arr, copy=True): Returns ------- result : ndarray with dtype timedelta64[ns] - """ return arr.astype(TD_DTYPE, copy=copy) + # TODO: check for overflows when going from a lower-resolution to nanos @cython.boundscheck(False) @cython.wraparound(False) -def datetime_to_datetime64(object[:] values): +def datetime_to_datetime64(values: object[:]): """ Convert ndarray of datetime-like objects to int64 array representing nanosecond timestamps. @@ -614,6 +614,8 @@ cpdef inline datetime localize_pydatetime(datetime dt, object tz): # ---------------------------------------------------------------------- # Timezone Conversion +@cython.boundscheck(False) +@cython.wraparound(False) cdef inline int64_t[:] _tz_convert_dst(int64_t[:] values, tzinfo tz, bint to_utc=True): """ @@ -852,14 +854,14 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, localized : ndarray[int64_t] """ cdef: - ndarray[int64_t] trans + ndarray[int64_t] trans, result_a, result_b int64_t[:] deltas, idx_shifted - ndarray ambiguous_array - Py_ssize_t i, idx, pos, ntrans, n = len(vals) - Py_ssize_t delta_idx_offset, delta_idx - int64_t *tdata + int64_t[:] result, dst_hours, idx_shifted_left, idx_shifted_right int64_t v, left, right, val, v_left, v_right, new_local, remaining_mins - ndarray[int64_t] result, result_a, result_b, dst_hours + Py_ssize_t i, idx, pos, pos_left, pos_right, ntrans, n = len(vals) + Py_ssize_t delta_idx, delta_idx_offset + int64_t *tdata + ndarray ambiguous_array npy_datetimestruct dts bint infer_dst = False, is_dst = False, fill = False bint shift = False, fill_nonexist = False @@ -874,7 +876,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, for i in range(n): v = vals[i] result[i] = _tz_convert_tzlocal_utc(v, tz, to_utc=True) - return result + return np.asarray(result) if is_string_object(ambiguous): if ambiguous == 'infer': @@ -936,7 +938,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, if infer_dst: dst_hours = np.empty(n, dtype=np.int64) - dst_hours.fill(NPY_NAT) + dst_hours[:] = NPY_NAT # Get the ambiguous hours (given the above, these are the hours # where result_a != result_b and neither of them are NAT) @@ -977,7 +979,13 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, # Pull the only index and adjust a_idx = grp[:switch_idx] b_idx = grp[switch_idx:] - dst_hours[grp] = np.hstack((result_a[a_idx], result_b[b_idx])) + + # __setitem__ on dst_hours.base because indexing with + # an ndarray (grp) directly on a memoryview is not supported + # TODO: is `grp` necessarily contiguous? i.e. could we + # equivalently write dst_hours[grp[0]:grp[-1]] = ... ? + dst_hours.base[grp] = np.hstack((result_a[a_idx], + result_b[b_idx])) for i in range(n): val = vals[i] @@ -1026,7 +1034,7 @@ def tz_localize_to_utc(ndarray[int64_t] vals, object tz, object ambiguous=None, stamp = _render_tstamp(val) raise pytz.NonExistentTimeError(stamp) - return result + return np.asarray(result) cdef inline bisect_right_i8(int64_t *data, int64_t val, Py_ssize_t n): @@ -1128,7 +1136,7 @@ def normalize_i8_timestamps(int64_t[:] stamps, object tz=None): dt64_to_dtstruct(stamps[i], &dts) result[i] = _normalized_stamp(&dts) - return result.base # .base to access underlying np.ndarray + return np.asarray(result) @cython.wraparound(False) @@ -1220,7 +1228,7 @@ cdef inline int64_t _normalized_stamp(npy_datetimestruct *dts) nogil: @cython.wraparound(False) @cython.boundscheck(False) -def is_date_array_normalized(int64_t[:] stamps, tz=None): +def is_date_array_normalized(int64_t[:] stamps, object tz=None): """ Check if all of the given (nanosecond) timestamps are normalized to midnight, i.e. hour == minute == second == 0. If the optional timezone diff --git a/pandas/_libs/tslibs/fields.pyx b/pandas/_libs/tslibs/fields.pyx index 216128cae2002..50aa22ae49d7c 100644 --- a/pandas/_libs/tslibs/fields.pyx +++ b/pandas/_libs/tslibs/fields.pyx @@ -49,7 +49,7 @@ def build_field_sarray(int64_t[:] dtindex): cdef: Py_ssize_t i, count = 0 npy_datetimestruct dts - ndarray[int32_t] years, months, days, hours, minutes, seconds, mus + int32_t[:] years, months, days, hours, minutes, seconds, mus count = len(dtindex) @@ -93,7 +93,7 @@ def get_date_name_field(int64_t[:] dtindex, object field, object locale=None): """ cdef: Py_ssize_t i, count = 0 - ndarray[object] out, names + object[:] out, names npy_datetimestruct dts int dow @@ -114,7 +114,7 @@ def get_date_name_field(int64_t[:] dtindex, object field, object locale=None): dt64_to_dtstruct(dtindex[i], &dts) dow = dayofweek(dts.year, dts.month, dts.day) out[i] = names[dow].capitalize() - return out + elif field == 'month_name': if locale is None: names = np.array(MONTHS_FULL, dtype=np.object_) @@ -128,12 +128,15 @@ def get_date_name_field(int64_t[:] dtindex, object field, object locale=None): dt64_to_dtstruct(dtindex[i], &dts) out[i] = names[dts.month].capitalize() - return out - raise ValueError("Field %s not supported" % field) + else: + raise ValueError("Field {field} not supported".format(field=field)) + + return np.asarray(out) @cython.wraparound(False) +@cython.boundscheck(False) def get_start_end_field(int64_t[:] dtindex, object field, object freqstr=None, int month_kw=12): """ @@ -147,8 +150,8 @@ def get_start_end_field(int64_t[:] dtindex, object field, bint is_business = 0 int end_month = 12 int start_month = 1 - ndarray[int8_t] out - ndarray[int32_t, ndim=2] _month_offset + int8_t[:] out + int32_t[:, :] _month_offset bint isleap npy_datetimestruct dts int mo_off, dom, doy, dow, ldom @@ -163,8 +166,8 @@ def get_start_end_field(int64_t[:] dtindex, object field, if freqstr: if freqstr == 'C': - raise ValueError( - "Custom business days is not supported by %s" % field) + raise ValueError("Custom business days is not supported " + "by {field}".format(field=field)) is_business = freqstr[0] == 'B' # YearBegin(), BYearBegin() use month = starting month of year. @@ -196,7 +199,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, if (dom == 1 and dow < 5) or (dom <= 3 and dow == 0): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -208,7 +211,6 @@ def get_start_end_field(int64_t[:] dtindex, object field, if dom == 1: out[i] = 1 - return out.view(bool) elif field == 'is_month_end': if is_business: @@ -228,7 +230,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, if (ldom == doy and dow < 5) or ( dow == 4 and (ldom - doy <= 2)): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -244,7 +246,6 @@ def get_start_end_field(int64_t[:] dtindex, object field, if ldom == doy: out[i] = 1 - return out.view(bool) elif field == 'is_quarter_start': if is_business: @@ -260,7 +261,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, if ((dts.month - start_month) % 3 == 0) and ( (dom == 1 and dow < 5) or (dom <= 3 and dow == 0)): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -272,7 +273,6 @@ def get_start_end_field(int64_t[:] dtindex, object field, if ((dts.month - start_month) % 3 == 0) and dom == 1: out[i] = 1 - return out.view(bool) elif field == 'is_quarter_end': if is_business: @@ -293,7 +293,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, (ldom == doy and dow < 5) or ( dow == 4 and (ldom - doy <= 2))): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -309,7 +309,6 @@ def get_start_end_field(int64_t[:] dtindex, object field, if ((dts.month - end_month) % 3 == 0) and (ldom == doy): out[i] = 1 - return out.view(bool) elif field == 'is_year_start': if is_business: @@ -325,7 +324,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, if (dts.month == start_month) and ( (dom == 1 and dow < 5) or (dom <= 3 and dow == 0)): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -337,7 +336,6 @@ def get_start_end_field(int64_t[:] dtindex, object field, if (dts.month == start_month) and dom == 1: out[i] = 1 - return out.view(bool) elif field == 'is_year_end': if is_business: @@ -358,7 +356,7 @@ def get_start_end_field(int64_t[:] dtindex, object field, (ldom == doy and dow < 5) or ( dow == 4 and (ldom - doy <= 2))): out[i] = 1 - return out.view(bool) + else: for i in range(count): if dtindex[i] == NPY_NAT: @@ -374,21 +372,23 @@ def get_start_end_field(int64_t[:] dtindex, object field, if (dts.month == end_month) and (ldom == doy): out[i] = 1 - return out.view(bool) - raise ValueError("Field %s not supported" % field) + else: + raise ValueError("Field {field} not supported".format(field=field)) + + return np.asarray(out).view(bool) @cython.wraparound(False) @cython.boundscheck(False) -def get_date_field(ndarray[int64_t] dtindex, object field): +def get_date_field(int64_t[:] dtindex, object field): """ Given a int64-based datetime index, extract the year, month, etc., field and return an array of these values. """ cdef: Py_ssize_t i, count = 0 - ndarray[int32_t] out + int32_t[:] out npy_datetimestruct dts count = len(dtindex) @@ -403,7 +403,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.year - return out elif field == 'M': with nogil: @@ -414,7 +413,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.month - return out elif field == 'D': with nogil: @@ -425,7 +423,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.day - return out elif field == 'h': with nogil: @@ -436,7 +433,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.hour - return out elif field == 'm': with nogil: @@ -447,7 +443,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.min - return out elif field == 's': with nogil: @@ -458,7 +453,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.sec - return out elif field == 'us': with nogil: @@ -469,7 +463,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.us - return out elif field == 'ns': with nogil: @@ -480,7 +473,7 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.ps / 1000 - return out + elif field == 'doy': with nogil: for i in range(count): @@ -490,7 +483,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = get_day_of_year(dts.year, dts.month, dts.day) - return out elif field == 'dow': with nogil: @@ -501,7 +493,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dayofweek(dts.year, dts.month, dts.day) - return out elif field == 'woy': with nogil: @@ -512,7 +503,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = get_week_of_year(dts.year, dts.month, dts.day) - return out elif field == 'q': with nogil: @@ -524,7 +514,6 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = dts.month out[i] = ((out[i] - 1) / 3) + 1 - return out elif field == 'dim': with nogil: @@ -535,11 +524,14 @@ def get_date_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) out[i] = get_days_in_month(dts.year, dts.month) - return out + elif field == 'is_leap_year': return isleapyear_arr(get_date_field(dtindex, 'Y')) - raise ValueError("Field %s not supported" % field) + else: + raise ValueError("Field %s not supported" % field) + + return np.asarray(out) @cython.wraparound(False) @@ -551,7 +543,7 @@ def get_timedelta_field(int64_t[:] tdindex, object field): """ cdef: Py_ssize_t i, count = 0 - ndarray[int32_t] out + int32_t[:] out pandas_timedeltastruct tds count = len(tdindex) @@ -566,7 +558,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.days - return out elif field == 'h': with nogil: @@ -577,7 +568,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.hrs - return out elif field == 's': with nogil: @@ -588,7 +578,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.sec - return out elif field == 'seconds': with nogil: @@ -599,7 +588,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.seconds - return out elif field == 'ms': with nogil: @@ -610,7 +598,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.ms - return out elif field == 'microseconds': with nogil: @@ -621,7 +608,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.microseconds - return out elif field == 'us': with nogil: @@ -632,7 +618,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.us - return out elif field == 'ns': with nogil: @@ -643,7 +628,6 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.ns - return out elif field == 'nanoseconds': with nogil: @@ -654,9 +638,11 @@ def get_timedelta_field(int64_t[:] tdindex, object field): td64_to_tdstruct(tdindex[i], &tds) out[i] = tds.nanoseconds - return out - raise ValueError("Field %s not supported" % field) + else: + raise ValueError("Field {field} not supported".format(field=field)) + + return np.asarray(out) cpdef isleapyear_arr(ndarray years): diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 528d30ddd7205..3af4107a333f8 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -816,7 +816,8 @@ def shift_months(int64_t[:] dtindex, int months, object day=None): return np.asarray(out) -cpdef datetime shift_month(datetime stamp, int months, object day_opt=None): +def shift_month(stamp: datetime, months: int, + day_opt: object = None) -> datetime: """ Given a datetime (or Timestamp) `stamp`, an integer `months` and an option `day_opt`, return a new datetimelike that many months later, @@ -946,8 +947,8 @@ cpdef int roll_convention(int other, int n, int compare) nogil: return n -cpdef int roll_qtrday(datetime other, int n, int month, object day_opt, - int modby=3) except? -1: +def roll_qtrday(other: datetime, n: int, month: int, + day_opt: object, modby: int = 3) -> int: """ Possibly increment or decrement the number of periods to shift based on rollforward/rollbackward conventions. diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index a769bbb081398..4eaeac66fda01 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# cython: boundscheck=False, wraparound=False from datetime import datetime, date from cpython cimport ( @@ -1002,7 +1003,7 @@ def dt64arr_to_periodarr(int64_t[:] dtarr, int freq, tz=None): out[i] = get_period_ordinal(&dts, freq) else: out = localize_dt64arr_to_period(dtarr, freq, tz) - return out.base # .base to access underlying np.ndarray + return np.asarray(out) @cython.wraparound(False) @@ -1027,7 +1028,7 @@ def periodarr_to_dt64arr(int64_t[:] periodarr, int freq): continue out[i] = period_ordinal_to_dt64(periodarr[i], freq) - return out.base # .base to access underlying np.ndarray + return np.asarray(out) cpdef int64_t period_asfreq(int64_t ordinal, int freq1, int freq2, bint end): @@ -1107,7 +1108,7 @@ cdef inline int calc_week_end(int freq, int group) nogil: @cython.wraparound(False) @cython.boundscheck(False) -def period_asfreq_arr(ndarray[int64_t] arr, int freq1, int freq2, bint end): +def period_asfreq_arr(int64_t[:] arr, int freq1, int freq2, bint end): """ Convert int64-array of period ordinals from one frequency to another, and if upsampling, choose to use start ('S') or end ('E') of period. @@ -1125,23 +1126,15 @@ def period_asfreq_arr(ndarray[int64_t] arr, int freq1, int freq2, bint end): func = get_asfreq_func(freq1, freq2) get_asfreq_info(freq1, freq2, end, &af_info) - mask = arr == NPY_NAT - if mask.any(): # NaT process - for i in range(n): - val = arr[i] - if val != NPY_NAT: - val = func(val, &af_info) - if val == INT32_MIN: - raise ValueError("Unable to convert to desired frequency.") - result[i] = val - else: - for i in range(n): - val = func(arr[i], &af_info) + for i in range(n): + val = arr[i] + if val != NPY_NAT: + val = func(val, &af_info) if val == INT32_MIN: raise ValueError("Unable to convert to desired frequency.") - result[i] = val + result[i] = val - return result.base # .base to access underlying np.ndarray + return np.asarray(result) cpdef int64_t period_ordinal(int y, int m, int d, int h, int min, @@ -1240,6 +1233,7 @@ cdef list extra_fmts = [(b"%q", b"^`AB`^"), cdef list str_extra_fmts = ["^`AB`^", "^`CD`^", "^`EF`^", "^`GH`^", "^`IJ`^", "^`KL`^"] + cdef object _period_strftime(int64_t value, int freq, object fmt): cdef: Py_ssize_t i @@ -1403,7 +1397,7 @@ def get_period_field_arr(int code, int64_t[:] arr, int freq): continue out[i] = func(arr[i], freq) - return out.base # .base to access underlying np.ndarray + return np.asarray(out) cdef accessor _get_accessor_func(int code): @@ -1465,7 +1459,7 @@ def extract_ordinals(object[:] values, freq): else: ordinals[i] = p.ordinal - return ordinals.base # .base to access underlying np.ndarray + return np.asarray(ordinals) def extract_freq(object[:] values): diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 74bbc64af25f2..75394f0794708 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -6,6 +6,7 @@ import warnings import sys cdef bint PY3 = (sys.version_info[0] >= 3) +import cython from cython import Py_ssize_t from cpython cimport Py_NE, Py_EQ, PyObject_RichCompare @@ -82,7 +83,9 @@ _no_input = object() # ---------------------------------------------------------------------- # API -def ints_to_pytimedelta(int64_t[:] arr, box=False): +@cython.boundscheck(False) +@cython.wraparound(False) +def ints_to_pytimedelta(int64_t[:] arr, bint box=False): """ convert an i8 repr to an ndarray of timedelta or Timedelta (if box == True) @@ -198,6 +201,8 @@ cpdef convert_to_timedelta64(object ts, object unit): return ts.astype('timedelta64[ns]') +@cython.boundscheck(False) +@cython.wraparound(False) def array_to_timedelta64(object[:] values, unit='ns', errors='raise'): """ Convert an ndarray to an array of timedeltas. If errors == 'coerce', diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index 65da765bae739..73a59d75cae2a 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -968,49 +968,49 @@ class Timestamp(_Timestamp): return getattr(self.freq, 'freqstr', self.freq) @property - def is_month_start(self): + def is_month_start(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return self.day == 1 return self._get_start_end_field('is_month_start') @property - def is_month_end(self): + def is_month_end(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return self.day == self.days_in_month return self._get_start_end_field('is_month_end') @property - def is_quarter_start(self): + def is_quarter_start(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return self.day == 1 and self.month % 3 == 1 return self._get_start_end_field('is_quarter_start') @property - def is_quarter_end(self): + def is_quarter_end(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return (self.month % 3) == 0 and self.day == self.days_in_month return self._get_start_end_field('is_quarter_end') @property - def is_year_start(self): + def is_year_start(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return self.day == self.month == 1 return self._get_start_end_field('is_year_start') @property - def is_year_end(self): + def is_year_end(self) -> bool: if self.freq is None: # fast-path for non-business frequencies return self.month == 12 and self.day == 31 return self._get_start_end_field('is_year_end') @property - def is_leap_year(self): + def is_leap_year(self) -> bool: return bool(ccalendar.is_leapyear(self.year)) def tz_localize(self, tz, ambiguous='raise', nonexistent='raise',