From ae142346391355dbb08170108100d1cebc3fb20f Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 26 Nov 2017 22:27:43 -0800 Subject: [PATCH 1/6] implement shift_quarters, make get_first/last_bday nogil --- pandas/_libs/tslibs/offsets.pyx | 278 +++++++++++++----- .../tests/tseries/offsets/test_liboffsets.py | 13 +- .../tests/tseries/offsets/test_yqm_offsets.py | 6 +- pandas/tseries/offsets.py | 113 +++---- 4 files changed, 280 insertions(+), 130 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 654c51f0ca842..33f3f0440d188 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -17,14 +17,12 @@ np.import_array() from util cimport is_string_object, is_integer_object -from pandas._libs.tslib import monthrange - from conversion cimport tz_convert_single, pydt_to_i8 from frequencies cimport get_freq_code from nattype cimport NPY_NAT from np_datetime cimport (pandas_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct, - is_leapyear, days_per_month_table) + is_leapyear, days_per_month_table, dayofweek) # --------------------------------------------------------------------- # Constants @@ -145,45 +143,44 @@ def apply_index_wraps(func): # --------------------------------------------------------------------- # Business Helpers -cpdef int get_lastbday(int wkday, int days_in_month): +cpdef int get_lastbday(int year, int month) nogil: """ Find the last day of the month that is a business day. - (wkday, days_in_month) is the output from monthrange(year, month) - Parameters ---------- - wkday : int - days_in_month : int + 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 wkday, int days_in_month=0): +cpdef int get_firstbday(int year, int month) nogil: """ Find the first day of the month that is a business day. - (wkday, days_in_month) is the output from monthrange(year, month) - Parameters ---------- - wkday : int - days_in_month : int, default 0 + year : int + month : int Returns ------- first_bday : int - - Notes - ----- - `days_in_month` arg is a dummy so that this has the same signature as - `get_lastbday`. """ - cdef int first + cdef: + int first, wkday + + wkday = dayofweek(year, month, 1) first = 1 if wkday == 5: # on Saturday first = 3 @@ -472,6 +469,154 @@ cdef inline int month_add_months(pandas_datetimestruct dts, int months) nogil: return 12 if new_month == 0 else new_month +@cython.wraparound(False) +@cython.boundscheck(False) +def shift_quarters(int64_t[:] dtindex, int quarters, + int q1start_month, object day, int modby=3): + """ + + Parameters + ---------- + dtindex : int64_t[:] timestamps for input dates + quarters : int number of quarters to shift + q1start_month : int month in which Q1 begins by convention + day : {'start', 'end', 'business_start', 'business_end'} + + """ + cdef: + Py_ssize_t i + pandas_datetimestruct dts + int count = len(dtindex) + int months_to_roll, months_since, n, compare_day + bint roll_check + int64_t[:] out = np.empty(count, dtype='int64') + + if day == 'start': + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue + + dt64_to_dtstruct(dtindex[i], &dts) + n = quarters + + months_since = (dts.month - q1start_month) % modby + + # offset semantics - if on the anchor point and going backwards + # shift to next + if n <= 0 and (months_since != 0 or + (months_since == 0 and dts.day > 1)): + n += 1 + + dts.year = year_add_months(dts, modby * n - months_since) + dts.month = month_add_months(dts, modby * n - months_since) + dts.day = 1 + + out[i] = dtstruct_to_dt64(&dts) + + elif day == 'end': + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue + + dt64_to_dtstruct(dtindex[i], &dts) + n = quarters + + months_since = (dts.month - q1start_month) % modby + + if n <= 0 and months_since != 0: + # The general case of this condition would be + # `months_since != 0 or (months_since == 0 and + # dts.day > get_days_in_month(dts.year, dts.month))` + # but the get_days_in_month inequality would never hold. + n += 1 + elif n > 0 and (months_since == 0 and + dts.day < get_days_in_month(dts.year, + dts.month)): + n -= 1 + + dts.year = year_add_months(dts, modby * n - months_since) + dts.month = month_add_months(dts, modby * n - months_since) + dts.day = get_days_in_month(dts.year, dts.month) + + out[i] = dtstruct_to_dt64(&dts) + + elif day == 'business_start': + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue + + dt64_to_dtstruct(dtindex[i], &dts) + n = quarters + + months_since = (dts.month - q1start_month) % modby + compare_month = dts.month - months_since + compare_month = compare_month or 12 + ## compare_day is only relevant for comparison in the case + ## where months_since == 0. + compare_day = get_firstbday(dts.year, compare_month) + + if n <= 0 and (months_since != 0 or + (months_since == 0 and dts.day > compare_day)): + # make sure to roll forward, so negate + n += 1 + elif n > 0 and (months_since == 0 and dts.day < compare_day): + # pretend to roll back if on same month but + # before compare_day + n -= 1 + + dts.year = year_add_months(dts, modby * n - months_since) + dts.month = month_add_months(dts, modby * n - months_since) + + dts.day = get_firstbday(dts.year, dts.month) + + out[i] = dtstruct_to_dt64(&dts) + + elif day == 'business_end': + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue + + dt64_to_dtstruct(dtindex[i], &dts) + n = quarters + + months_since = (dts.month - q1start_month) % modby + compare_month = dts.month - months_since + compare_month = compare_month or 12 + # compare_day is only relevant for comparison in the case + # where months_since == 0. + compare_day = get_lastbday(dts.year, compare_month) + + if n <= 0 and (months_since != 0 or + (months_since == 0 and dts.day > compare_day)): + # make sure to roll forward, so negate + n += 1 + elif n > 0 and (months_since == 0 and dts.day < compare_day): + # pretend to roll back if on same month but + # before compare_day + n -= 1 + + dts.year = year_add_months(dts, modby * n - months_since) + dts.month = month_add_months(dts, modby * n - months_since) + + dts.day = get_lastbday(dts.year, dts.month) + + out[i] = dtstruct_to_dt64(&dts) + + else: + raise ValueError("day must be None, 'start', 'end', " + "'business_start', or 'business_end'") + + return np.asarray(out) + + @cython.wraparound(False) @cython.boundscheck(False) def shift_months(int64_t[:] dtindex, int months, object day=None): @@ -556,52 +701,50 @@ def shift_months(int64_t[:] dtindex, int months, object day=None): out[i] = dtstruct_to_dt64(&dts) elif day == 'business_start': - for i in range(count): - if dtindex[i] == NPY_NAT: - out[i] = NPY_NAT - continue + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue - dt64_to_dtstruct(dtindex[i], &dts) - months_to_roll = months - wkday, days_in_month = monthrange(dts.year, dts.month) - compare_day = get_firstbday(wkday, days_in_month) + dt64_to_dtstruct(dtindex[i], &dts) + months_to_roll = months + compare_day = get_firstbday(dts.year, dts.month) - if months_to_roll > 0 and dts.day < compare_day: - months_to_roll -= 1 - elif months_to_roll <= 0 and dts.day > compare_day: - # as if rolled forward already - months_to_roll += 1 + if months_to_roll > 0 and dts.day < compare_day: + months_to_roll -= 1 + elif months_to_roll <= 0 and dts.day > compare_day: + # as if rolled forward already + months_to_roll += 1 - dts.year = year_add_months(dts, months_to_roll) - dts.month = month_add_months(dts, months_to_roll) + dts.year = year_add_months(dts, months_to_roll) + dts.month = month_add_months(dts, months_to_roll) - wkday, days_in_month = monthrange(dts.year, dts.month) - dts.day = get_firstbday(wkday, days_in_month) - out[i] = dtstruct_to_dt64(&dts) + dts.day = get_firstbday(dts.year, dts.month) + out[i] = dtstruct_to_dt64(&dts) elif day == 'business_end': - for i in range(count): - if dtindex[i] == NPY_NAT: - out[i] = NPY_NAT - continue + with nogil: + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = NPY_NAT + continue - dt64_to_dtstruct(dtindex[i], &dts) - months_to_roll = months - wkday, days_in_month = monthrange(dts.year, dts.month) - compare_day = get_lastbday(wkday, days_in_month) + dt64_to_dtstruct(dtindex[i], &dts) + months_to_roll = months + compare_day = get_lastbday(dts.year, dts.month) - if months_to_roll > 0 and dts.day < compare_day: - months_to_roll -= 1 - elif months_to_roll <= 0 and dts.day > compare_day: - # as if rolled forward already - months_to_roll += 1 + if months_to_roll > 0 and dts.day < compare_day: + months_to_roll -= 1 + elif months_to_roll <= 0 and dts.day > compare_day: + # as if rolled forward already + months_to_roll += 1 - dts.year = year_add_months(dts, months_to_roll) - dts.month = month_add_months(dts, months_to_roll) + dts.year = year_add_months(dts, months_to_roll) + dts.month = month_add_months(dts, months_to_roll) - wkday, days_in_month = monthrange(dts.year, dts.month) - dts.day = get_lastbday(wkday, days_in_month) - out[i] = dtstruct_to_dt64(&dts) + dts.day = get_lastbday(dts.year, dts.month) + out[i] = dtstruct_to_dt64(&dts) else: raise ValueError("day must be None, 'start', 'end', " @@ -635,7 +778,7 @@ cpdef datetime shift_month(datetime stamp, int months, object day_opt=None): """ cdef: int year, month, day - int wkday, days_in_month, dy + int days_in_month, dy dy = (stamp.month + months) // 12 month = (stamp.month + months) % 12 @@ -645,20 +788,21 @@ cpdef datetime shift_month(datetime stamp, int months, object day_opt=None): dy -= 1 year = stamp.year + dy - wkday, days_in_month = monthrange(year, month) if day_opt is None: + days_in_month = get_days_in_month(year, month) day = min(stamp.day, days_in_month) elif day_opt == 'start': day = 1 elif day_opt == 'end': - day = days_in_month + day = get_days_in_month(year, month) elif day_opt == 'business_start': # first business day of month - day = get_firstbday(wkday, days_in_month) + day = get_firstbday(year, month) elif day_opt == 'business_end': # last business day of month - day = get_lastbday(wkday, days_in_month) + day = get_lastbday(year, month) elif is_integer_object(day_opt): + days_in_month = get_days_in_month(year, month) day = min(day_opt, days_in_month) else: raise ValueError(day_opt) @@ -691,22 +835,22 @@ cpdef int get_day_of_month(datetime other, day_opt) except? -1: """ cdef: - int wkday, days_in_month + int days_in_month if day_opt == 'start': return 1 - - wkday, days_in_month = monthrange(other.year, other.month) - if day_opt == 'end': + elif day_opt == 'end': + days_in_month = get_days_in_month(other.year, other.month) return days_in_month elif day_opt == 'business_start': # first business day of month - return get_firstbday(wkday, days_in_month) + return get_firstbday(other.year, other.month) elif day_opt == 'business_end': # last business day of month - return get_lastbday(wkday, days_in_month) + return get_lastbday(other.year, other.month) elif is_integer_object(day_opt): - day = min(day_opt, days_in_month) + days_in_month = get_days_in_month(other.year, other.month) + return min(day_opt, days_in_month) elif day_opt is None: # Note: unlike `shift_month`, get_day_of_month does not # allow day_opt = None diff --git a/pandas/tests/tseries/offsets/test_liboffsets.py b/pandas/tests/tseries/offsets/test_liboffsets.py index 321104222936b..8aa32bc600ee6 100644 --- a/pandas/tests/tseries/offsets/test_liboffsets.py +++ b/pandas/tests/tseries/offsets/test_liboffsets.py @@ -6,7 +6,6 @@ import pytest -from pandas._libs import tslib from pandas import Timestamp import pandas._libs.tslibs.offsets as liboffsets @@ -15,25 +14,21 @@ def test_get_lastbday(): dt = datetime(2017, 11, 30) assert dt.weekday() == 3 # i.e. this is a business day - wkday, days_in_month = tslib.monthrange(dt.year, dt.month) - assert liboffsets.get_lastbday(wkday, days_in_month) == 30 + assert liboffsets.get_lastbday(dt.year, dt.month) == 30 dt = datetime(1993, 10, 31) assert dt.weekday() == 6 # i.e. this is not a business day - wkday, days_in_month = tslib.monthrange(dt.year, dt.month) - assert liboffsets.get_lastbday(wkday, days_in_month) == 29 + assert liboffsets.get_lastbday(dt.year, dt.month) == 29 def test_get_firstbday(): dt = datetime(2017, 4, 1) assert dt.weekday() == 5 # i.e. not a weekday - wkday, days_in_month = tslib.monthrange(dt.year, dt.month) - assert liboffsets.get_firstbday(wkday, days_in_month) == 3 + assert liboffsets.get_firstbday(dt.year, dt.month) == 3 dt = datetime(1993, 10, 1) assert dt.weekday() == 4 # i.e. a business day - wkday, days_in_month = tslib.monthrange(dt.year, dt.month) - assert liboffsets.get_firstbday(wkday, days_in_month) == 1 + assert liboffsets.get_firstbday(dt.year, dt.month) == 1 def test_shift_month(): diff --git a/pandas/tests/tseries/offsets/test_yqm_offsets.py b/pandas/tests/tseries/offsets/test_yqm_offsets.py index 292dd5eba938e..b5a0e0efe5b03 100644 --- a/pandas/tests/tseries/offsets/test_yqm_offsets.py +++ b/pandas/tests/tseries/offsets/test_yqm_offsets.py @@ -34,7 +34,11 @@ def test_quarterly_dont_normalize(): @pytest.mark.parametrize('offset', [MonthBegin(), MonthEnd(), - BMonthBegin(), BMonthEnd()]) + BMonthBegin(), BMonthEnd(), + QuarterBegin(), QuarterEnd(), + BQuarterBegin(), BQuarterEnd(), + YearBegin(), YearEnd(), + BYearBegin(), BYearEnd()]) def test_apply_index(offset): rng = pd.date_range(start='1/1/2000', periods=100000, freq='T') ser = pd.Series(rng) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 8e1ead5dfbe9e..ebe15f4c414f5 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -7,7 +7,8 @@ from pandas import compat import numpy as np -from pandas.core.dtypes.generic import ABCSeries, ABCDatetimeIndex, ABCPeriod +from pandas.core.dtypes.generic import (ABCSeries, ABCDatetimeIndex, ABCPeriod, + ABCIndexClass) from pandas.core.tools.datetimes import to_datetime, normalize_date from pandas.core.common import AbstractMethodError @@ -1028,10 +1029,7 @@ def cbday(self): @cache_readonly def m_offset(self): - kwds = self.kwds - kwds = {key: kwds[key] for key in kwds - if key not in ['calendar', 'weekmask', 'holidays', 'offset']} - return MonthEnd(n=1, normalize=self.normalize, **kwds) + return MonthEnd(n=1, normalize=self.normalize) @apply_wraps def apply(self, other): @@ -1106,10 +1104,7 @@ def cbday(self): @cache_readonly def m_offset(self): - kwds = self.kwds - kwds = {key: kwds[key] for key in kwds - if key not in ['calendar', 'weekmask', 'holidays', 'offset']} - return MonthBegin(n=1, normalize=self.normalize, **kwds) + return MonthBegin(n=1, normalize=self.normalize) @apply_wraps def apply(self, other): @@ -1254,12 +1249,9 @@ def onOffset(self, dt): def _apply(self, n, other): # if other.day is not day_of_month move to day_of_month and update n - if other.day < self.day_of_month: - other = other.replace(day=self.day_of_month) - if n > 0: - n -= 1 + if n > 0 and other.day < self.day_of_month: + n -= 1 elif other.day > self.day_of_month: - other = other.replace(day=self.day_of_month) n += 1 months = n // 2 @@ -1309,12 +1301,9 @@ def onOffset(self, dt): def _apply(self, n, other): # if other.day is not day_of_month move to day_of_month and update n if other.day < self.day_of_month: - other = other.replace(day=self.day_of_month) n -= 1 - elif other.day > self.day_of_month: - other = other.replace(day=self.day_of_month) - if n <= 0: - n += 1 + elif n <= 0 and other.day > self.day_of_month: + n += 1 months = n // 2 + n % 2 day = 1 if n % 2 else self.day_of_month @@ -1471,6 +1460,7 @@ def apply(self, other): def getOffsetOfMonth(self, dt): w = Week(weekday=self.weekday) d = datetime(dt.year, dt.month, 1, tzinfo=dt.tzinfo) + # TODO: Is this DST-safe? d = w.rollforward(d) return d + timedelta(weeks=self.week) @@ -1550,6 +1540,7 @@ def getOffsetOfMonth(self, dt): d = datetime(dt.year, dt.month, 1, dt.hour, dt.minute, dt.second, dt.microsecond, tzinfo=dt.tzinfo) eom = m.rollforward(d) + # TODO: Is this DST-safe? w = Week(weekday=self.weekday) return w.rollback(eom) @@ -1635,6 +1626,12 @@ def onOffset(self, dt): modMonth = (dt.month - self.startingMonth) % 3 return modMonth == 0 and dt.day == self._get_offset_day(dt) + @apply_index_wraps + def apply_index(self, dtindex): + shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, + self.startingMonth, self._day_opt) + return dtindex._shallow_copy(shifted) + class BQuarterEnd(QuarterOffset): """DateOffset increments between business Quarter dates @@ -1670,10 +1667,6 @@ class QuarterEnd(EndMixin, QuarterOffset): _prefix = 'Q' _day_opt = 'end' - @apply_index_wraps - def apply_index(self, i): - return self._end_apply_index(i, self.freqstr) - class QuarterBegin(BeginMixin, QuarterOffset): _outputName = 'QuarterBegin' @@ -1682,13 +1675,6 @@ class QuarterBegin(BeginMixin, QuarterOffset): _prefix = 'QS' _day_opt = 'start' - @apply_index_wraps - def apply_index(self, i): - freq_month = 12 if self.startingMonth == 1 else self.startingMonth - 1 - month = liboffsets._int_to_month[freq_month] - freqstr = 'Q-{month}'.format(month=month) - return self._beg_apply_index(i, freqstr) - # --------------------------------------------------------------------- # Year-Based Offset Classes @@ -1709,6 +1695,13 @@ def apply(self, other): months = years * 12 + (self.month - other.month) return shift_month(other, months, self._day_opt) + @apply_index_wraps + def apply_index(self, dtindex): + shifted = liboffsets.shift_quarters(dtindex.asi8, self.n, + self.month, self._day_opt, + modby=12) + return dtindex._shallow_copy(shifted) + def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False @@ -1758,11 +1751,6 @@ class YearEnd(EndMixin, YearOffset): _prefix = 'A' _day_opt = 'end' - @apply_index_wraps - def apply_index(self, i): - # convert month anchor to annual period tuple - return self._end_apply_index(i, self.freqstr) - class YearBegin(BeginMixin, YearOffset): """DateOffset increments between calendar year begin dates""" @@ -1770,13 +1758,6 @@ class YearBegin(BeginMixin, YearOffset): _prefix = 'AS' _day_opt = 'start' - @apply_index_wraps - def apply_index(self, i): - freq_month = 12 if self.month == 1 else self.month - 1 - month = liboffsets._int_to_month[freq_month] - freqstr = 'A-{month}'.format(month=month) - return self._beg_apply_index(i, freqstr) - # --------------------------------------------------------------------- # Special Offset Classes @@ -2196,11 +2177,32 @@ def onOffset(self, dt): # Ticks -def _tick_comp(op): +def _tick_comp(cls, op): def f(self, other): - return op(self.delta, other.delta) + if isinstance(other, (Tick, timedelta, np.timedelta64)): + other_delta = delta_to_nanoseconds(other) + return op(self.nanos, other_delta) + + elif isinstance(other, Week) and other.weekday is None: + other_delta = int(timedelta(weeks=other.n).total_seconds() * 1e9) + return op(self.nanos, other_delta) - return f + elif isinstance(other, compat.string_types): + from pandas.tseries.frequencies import to_offset + other = to_offset(other) + if isinstance(other, DateOffset): + return f(self, other) + + elif isinstance(other, (np.ndarray, ABCIndexClass)): + # TODO: element-wise comparison + # defer to Timedelta implementation + return op(self.delta, other) + + raise TypeError('Cannot compare type {self} with type ' + '{other}'.format(self=type(self), other=type(other))) + + opname = '__{name}__'.format(name=op.__name__) + return compat.set_function_name(f, opname, cls) class Tick(SingleConstructorOffset): @@ -2213,12 +2215,12 @@ def __init__(self, n=1, normalize=False): self.normalize = normalize self.kwds = {} - __gt__ = _tick_comp(operator.gt) - __ge__ = _tick_comp(operator.ge) - __lt__ = _tick_comp(operator.lt) - __le__ = _tick_comp(operator.le) - __eq__ = _tick_comp(operator.eq) - __ne__ = _tick_comp(operator.ne) + @classmethod + def _make_comp_methods(cls): + cls.__gt__ = _tick_comp(cls, operator.gt) + cls.__ge__ = _tick_comp(cls, operator.ge) + cls.__lt__ = _tick_comp(cls, operator.lt) + cls.__le__ = _tick_comp(cls, operator.le) def __add__(self, other): if isinstance(other, Tick): @@ -2245,7 +2247,8 @@ def __eq__(self, other): if isinstance(other, Tick): return self.delta == other.delta else: - return DateOffset.__eq__(self, other) + # TODO: Are there cases where this should raise TypeError? + return False # This is identical to DateOffset.__hash__, but has to be redefined here # for Python 3, because we've redefined __eq__. @@ -2261,7 +2264,8 @@ def __ne__(self, other): if isinstance(other, Tick): return self.delta != other.delta else: - return DateOffset.__ne__(self, other) + # TODO: Are there cases where this should raise TypeError? + return True @property def delta(self): @@ -2300,6 +2304,9 @@ def isAnchored(self): return False +Tick._make_comp_methods() + + def _delta_to_tick(delta): if delta.microseconds == 0: if delta.seconds == 0: From 151ddae43039e6c3d6d6c15f5b1af679393fc69d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 27 Nov 2017 07:22:53 -0800 Subject: [PATCH 2/6] unmix begin/end --- pandas/tseries/offsets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index ebe15f4c414f5..f879116c10ad0 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -28,7 +28,7 @@ apply_index_wraps, roll_yearday, shift_month, - BeginMixin, EndMixin, + EndMixin, BaseOffset) @@ -1656,7 +1656,7 @@ class BQuarterBegin(QuarterOffset): _day_opt = 'business_start' -class QuarterEnd(EndMixin, QuarterOffset): +class QuarterEnd(QuarterOffset): """DateOffset increments between business Quarter dates startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... @@ -1668,7 +1668,7 @@ class QuarterEnd(EndMixin, QuarterOffset): _day_opt = 'end' -class QuarterBegin(BeginMixin, QuarterOffset): +class QuarterBegin(QuarterOffset): _outputName = 'QuarterBegin' _default_startingMonth = 3 _from_name_startingMonth = 1 @@ -1745,14 +1745,14 @@ class BYearBegin(YearOffset): _day_opt = 'business_start' -class YearEnd(EndMixin, YearOffset): +class YearEnd(YearOffset): """DateOffset increments between calendar year ends""" _default_month = 12 _prefix = 'A' _day_opt = 'end' -class YearBegin(BeginMixin, YearOffset): +class YearBegin(YearOffset): """DateOffset increments between calendar year begin dates""" _default_month = 1 _prefix = 'AS' From 21504bfd6dec479925e7b605d5dd471411218950 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 27 Nov 2017 07:31:48 -0800 Subject: [PATCH 3/6] comment fixup --- pandas/_libs/tslibs/offsets.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 33f3f0440d188..6aa74a27aede2 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -557,8 +557,8 @@ def shift_quarters(int64_t[:] dtindex, int quarters, months_since = (dts.month - q1start_month) % modby compare_month = dts.month - months_since compare_month = compare_month or 12 - ## compare_day is only relevant for comparison in the case - ## where months_since == 0. + # compare_day is only relevant for comparison in the case + # where months_since == 0. compare_day = get_firstbday(dts.year, compare_month) if n <= 0 and (months_since != 0 or From 4d35e0753547a8634e69bdcd9b1ad910a1dbe224 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 27 Nov 2017 08:08:18 -0800 Subject: [PATCH 4/6] flesh out docstring --- pandas/_libs/tslibs/offsets.pyx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 6aa74a27aede2..251af50ab12ce 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -474,6 +474,8 @@ cdef inline int month_add_months(pandas_datetimestruct dts, int months) nogil: def shift_quarters(int64_t[:] dtindex, int quarters, int q1start_month, object day, int modby=3): """ + Given an int64 array representing nanosecond timestamps, shift all elements + by the specified number of quarters using DateOffset semantics. Parameters ---------- @@ -481,7 +483,11 @@ def shift_quarters(int64_t[:] dtindex, int quarters, quarters : int number of quarters to shift q1start_month : int month in which Q1 begins by convention day : {'start', 'end', 'business_start', 'business_end'} + modby : int (3 for quarters, 12 for years) + Returns + ------- + out : ndarray[int64_t] """ cdef: Py_ssize_t i From d3244f522cd3acc54fda5b6a7e7d145624f7b2d0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 27 Nov 2017 08:17:49 -0800 Subject: [PATCH 5/6] revert changes out of scope to this PR --- pandas/tseries/offsets.py | 45 +++++++++------------------------------ 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index f879116c10ad0..a3cddaa19dc17 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -7,8 +7,7 @@ from pandas import compat import numpy as np -from pandas.core.dtypes.generic import (ABCSeries, ABCDatetimeIndex, ABCPeriod, - ABCIndexClass) +from pandas.core.dtypes.generic import ABCSeries, ABCDatetimeIndex, ABCPeriod from pandas.core.tools.datetimes import to_datetime, normalize_date from pandas.core.common import AbstractMethodError @@ -2177,32 +2176,11 @@ def onOffset(self, dt): # Ticks -def _tick_comp(cls, op): +def _tick_comp(op): def f(self, other): - if isinstance(other, (Tick, timedelta, np.timedelta64)): - other_delta = delta_to_nanoseconds(other) - return op(self.nanos, other_delta) + return op(self.delta, other.delta) - elif isinstance(other, Week) and other.weekday is None: - other_delta = int(timedelta(weeks=other.n).total_seconds() * 1e9) - return op(self.nanos, other_delta) - - elif isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - other = to_offset(other) - if isinstance(other, DateOffset): - return f(self, other) - - elif isinstance(other, (np.ndarray, ABCIndexClass)): - # TODO: element-wise comparison - # defer to Timedelta implementation - return op(self.delta, other) - - raise TypeError('Cannot compare type {self} with type ' - '{other}'.format(self=type(self), other=type(other))) - - opname = '__{name}__'.format(name=op.__name__) - return compat.set_function_name(f, opname, cls) + return f class Tick(SingleConstructorOffset): @@ -2215,12 +2193,12 @@ def __init__(self, n=1, normalize=False): self.normalize = normalize self.kwds = {} - @classmethod - def _make_comp_methods(cls): - cls.__gt__ = _tick_comp(cls, operator.gt) - cls.__ge__ = _tick_comp(cls, operator.ge) - cls.__lt__ = _tick_comp(cls, operator.lt) - cls.__le__ = _tick_comp(cls, operator.le) + __gt__ = _tick_comp(operator.gt) + __ge__ = _tick_comp(operator.ge) + __lt__ = _tick_comp(operator.lt) + __le__ = _tick_comp(operator.le) + __eq__ = _tick_comp(operator.eq) + __ne__ = _tick_comp(operator.ne) def __add__(self, other): if isinstance(other, Tick): @@ -2304,9 +2282,6 @@ def isAnchored(self): return False -Tick._make_comp_methods() - - def _delta_to_tick(delta): if delta.microseconds == 0: if delta.seconds == 0: From a8c6a9cd8aa01a3aa83cd910b8844f026c1f3952 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 27 Nov 2017 08:27:53 -0800 Subject: [PATCH 6/6] apply_index tests with negative n --- pandas/tests/tseries/offsets/test_yqm_offsets.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_yqm_offsets.py b/pandas/tests/tseries/offsets/test_yqm_offsets.py index b5a0e0efe5b03..22b8cf6119d18 100644 --- a/pandas/tests/tseries/offsets/test_yqm_offsets.py +++ b/pandas/tests/tseries/offsets/test_yqm_offsets.py @@ -33,13 +33,15 @@ def test_quarterly_dont_normalize(): assert (result.time() == date.time()) -@pytest.mark.parametrize('offset', [MonthBegin(), MonthEnd(), - BMonthBegin(), BMonthEnd(), - QuarterBegin(), QuarterEnd(), - BQuarterBegin(), BQuarterEnd(), - YearBegin(), YearEnd(), - BYearBegin(), BYearEnd()]) -def test_apply_index(offset): +@pytest.mark.parametrize('n', [-2, 1]) +@pytest.mark.parametrize('cls', [MonthBegin, MonthEnd, + BMonthBegin, BMonthEnd, + QuarterBegin, QuarterEnd, + BQuarterBegin, BQuarterEnd, + YearBegin, YearEnd, + BYearBegin, BYearEnd]) +def test_apply_index(cls, n): + offset = cls(n=n) rng = pd.date_range(start='1/1/2000', periods=100000, freq='T') ser = pd.Series(rng)