From 36c2ef1e87be5e8d3436cd64112043fd429b8dd9 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 19:44:58 -0700 Subject: [PATCH 01/11] use ccalendar instead of tslib.monthrange, cythonize bits of resolution --- pandas/_libs/tslib.pyx | 2 +- pandas/_libs/tslibs/offsets.pxd | 5 ++++ pandas/_libs/tslibs/offsets.pyx | 7 ++++++ pandas/_libs/tslibs/period.pyx | 24 ++++++++++++------- pandas/_libs/tslibs/resolution.pyx | 25 +++++++++++--------- pandas/core/indexes/datetimes.py | 5 ++-- pandas/tests/tseries/offsets/test_offsets.py | 6 ----- pandas/tseries/offsets.py | 6 ++--- 8 files changed, 48 insertions(+), 32 deletions(-) create mode 100644 pandas/_libs/tslibs/offsets.pxd diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 0f58cfa761f21..7462b1deb3625 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -127,7 +127,7 @@ def ints_to_pydatetime(ndarray[int64_t] arr, tz=None, freq=None, elif box == "datetime": func_create = create_datetime_from_ts else: - raise ValueError("box must be one of 'datetime', 'date', 'time' or" + + raise ValueError("box must be one of 'datetime', 'date', 'time' or" " 'timestamp'") if tz is not None: diff --git a/pandas/_libs/tslibs/offsets.pxd b/pandas/_libs/tslibs/offsets.pxd new file mode 100644 index 0000000000000..ec89e61aa867b --- /dev/null +++ b/pandas/_libs/tslibs/offsets.pxd @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# cython: profile=False + +cdef class _Tick(object): + pass diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 8caf9ea0e0389..f22aef2344608 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -395,6 +395,13 @@ class BaseOffset(_BaseOffset): return -self + other +cdef class _Tick(object): + # dummy class to mix in to offsets.Tick so that we can write cython + # code to check `isinstance(obj, _Tick)` instead of `isinstance(obj, Tick)`, + # i.e. avoid non-cython dependencies. + pass + + # ---------------------------------------------------------------------- # RelativeDelta Arithmetic diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 008747c0a9e78..6258916af4bf0 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -19,7 +19,7 @@ from pandas.compat import PY2 cimport cython -from cpython.datetime cimport PyDateTime_Check, PyDateTime_IMPORT +from cpython.datetime cimport PyDateTime_Check, PyDelta_Check, PyDateTime_IMPORT # import datetime C API PyDateTime_IMPORT @@ -51,13 +51,13 @@ from conversion cimport tz_convert_utc_to_tzlocal from frequencies cimport (get_freq_code, get_base_alias, get_to_timestamp_base, get_freq_str, get_rule_month) +#from offsets cimport _Tick from parsing import parse_time_string, NAT_SENTINEL from resolution import Resolution from nattype import nat_strings, NaT, iNaT from nattype cimport _nat_scalar_rules, NPY_NAT -from pandas.tseries import offsets -from pandas.tseries import frequencies +from pandas.tseries import frequencies, offsets cdef extern from "period_helper.h": @@ -1058,18 +1058,24 @@ cdef class _Period(object): return hash((self.ordinal, self.freqstr)) def _add_delta(self, other): - if isinstance(other, (timedelta, np.timedelta64, offsets.Tick)): + cdef: + int64_t nanos, offset_nanos + + if (PyDelta_Check(other) or util.is_timedelta64_object(other) or + isinstance(other, offsets.Tick)): offset = frequencies.to_offset(self.freq.rule_code) if isinstance(offset, offsets.Tick): nanos = delta_to_nanoseconds(other) offset_nanos = delta_to_nanoseconds(offset) + assert isinstance(self.freq, offsets.Tick), self.freq # pretty sure we can skip the frequencies.to_offset call + if nanos % offset_nanos == 0: ordinal = self.ordinal + (nanos // offset_nanos) return Period(ordinal=ordinal, freq=self.freq) msg = 'Input cannot be converted to Period(freq={0})' raise IncompatibleFrequency(msg.format(self.freqstr)) - elif isinstance(other, offsets.DateOffset): + elif getattr(other, "_typ", None) == "dateoffset": freqstr = other.rule_code base = get_base_alias(freqstr) if base == self.freq.rule_code: @@ -1082,8 +1088,8 @@ cdef class _Period(object): def __add__(self, other): if is_period_object(self): - if isinstance(other, (timedelta, np.timedelta64, - offsets.DateOffset)): + if (PyDelta_Check(other) or util.is_timedelta64_object(other) or + getattr(other, "_typ", None) == "dateoffset"): return self._add_delta(other) elif other is NaT: return NaT @@ -1109,8 +1115,8 @@ cdef class _Period(object): def __sub__(self, other): if is_period_object(self): - if isinstance(other, (timedelta, np.timedelta64, - offsets.DateOffset)): + if (PyDelta_Check(other) or util.is_timedelta64_object(other) or + getattr(other, "_typ", None) == "dateoffset"): neg_other = -other return self + neg_other elif util.is_integer_object(other): diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index d0a9501afe566..cd81734e77c7b 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -5,7 +5,7 @@ from cython cimport Py_ssize_t import numpy as np cimport numpy as cnp -from numpy cimport ndarray, int64_t +from numpy cimport ndarray, int64_t, int32_t cnp.import_array() from util cimport is_string_object, get_nat @@ -25,6 +25,7 @@ from fields import build_field_sarray from conversion import tz_convert from conversion cimport tz_convert_utc_to_tzlocal from ccalendar import MONTH_ALIASES, int_to_weekday +from ccalendar cimport get_days_in_month from pandas._libs.properties import cache_readonly from pandas._libs.tslib import Timestamp @@ -349,7 +350,7 @@ class Resolution(object): # TODO: this is non performant logic here (and duplicative) and this # simply should call unique_1d directly # plus no reason to depend on khash directly -cdef unique_deltas(ndarray[int64_t] arr): +cdef ndarray[int64_t, ndim=1] unique_deltas(ndarray[int64_t] arr): cdef: Py_ssize_t i, n = len(arr) int64_t val @@ -384,10 +385,11 @@ def _maybe_add_count(base, count): return base -class _FrequencyInferer(object): +cdef class _FrequencyInferer(object): """ Not sure if I can avoid the state machine here """ + cdef public index, values, warn, is_monotonic, _cache def __init__(self, index, warn=True): self.index = index @@ -475,19 +477,20 @@ class _FrequencyInferer(object): def rep_stamp(self): return Timestamp(self.values[0]) - def month_position_check(self): + cdef month_position_check(self): # TODO: cythonize this, very slow - calendar_end = True - business_end = True - calendar_start = True - business_start = True + cdef: + int32_t daysinmonth + bint calendar_end = True + bint business_end = True + bint calendar_start = True + bint business_start = True years = self.fields['Y'] months = self.fields['M'] days = self.fields['D'] weekdays = self.index.dayofweek - from calendar import monthrange for y, m, d, wd in zip(years, months, days, weekdays): if calendar_start: @@ -496,7 +499,7 @@ class _FrequencyInferer(object): business_start &= d == 1 or (d <= 3 and wd == 0) if calendar_end or business_end: - _, daysinmonth = monthrange(y, m) + daysinmonth = get_days_in_month(y, m) cal = d == daysinmonth if calendar_end: calendar_end &= cal @@ -591,7 +594,7 @@ class _FrequencyInferer(object): return {'cs': 'MS', 'bs': 'BMS', 'ce': 'M', 'be': 'BM'}.get(pos_check) - def _is_business_daily(self): + cdef bint _is_business_daily(self): # quick check: cannot be business daily if self.day_deltas != [1, 3]: return False diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index d44b13172f86d..66622814f172d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -55,6 +55,7 @@ from pandas._libs import (lib, index as libindex, tslib as libts, join as libjoin, Timestamp) from pandas._libs.tslibs import (timezones, conversion, fields, parsing, + ccalendar, resolution as libresolution) # -------- some conversion wrapper functions @@ -1451,14 +1452,14 @@ def _parsed_string_to_bounds(self, reso, parsed): Timestamp(datetime(parsed.year, 12, 31, 23, 59, 59, 999999), tz=self.tz)) elif reso == 'month': - d = libts.monthrange(parsed.year, parsed.month)[1] + d = ccalendar.get_days_in_month(parsed.year, parsed.month) return (Timestamp(datetime(parsed.year, parsed.month, 1), tz=self.tz), Timestamp(datetime(parsed.year, parsed.month, d, 23, 59, 59, 999999), tz=self.tz)) elif reso == 'quarter': qe = (((parsed.month - 1) + 2) % 12) + 1 # two months ahead - d = libts.monthrange(parsed.year, qe)[1] # at end of month + d = ccalendar.get_days_in_month(parsed.year, qe) # at end of month return (Timestamp(datetime(parsed.year, parsed.month, 1), tz=self.tz), Timestamp(datetime(parsed.year, qe, d, 23, 59, diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 5369b1a94a956..a1c5a825054ec 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -41,12 +41,6 @@ from .common import assert_offset_equal, assert_onOffset -def test_monthrange(): - import calendar - for y in range(2000, 2013): - for m in range(1, 13): - assert tslib.monthrange(y, m) == calendar.monthrange(y, m) - #### # Misc function tests #### diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index c294110d89ec5..7f5bd67a85ae3 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1140,7 +1140,7 @@ def apply(self, other): # shift `other` to self.day_of_month, incrementing `n` if necessary n = liboffsets.roll_convention(other.day, self.n, self.day_of_month) - days_in_month = tslib.monthrange(other.year, other.month)[1] + days_in_month = ccalendar.get_days_in_month(other.year, other.month) # For SemiMonthBegin on other.day == 1 and # SemiMonthEnd on other.day == days_in_month, @@ -1217,7 +1217,7 @@ class SemiMonthEnd(SemiMonthOffset): def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - _, days_in_month = tslib.monthrange(dt.year, dt.month) + days_in_month = ccalendar.get_days_in_month(dt.year, dt.month) return dt.day in (self.day_of_month, days_in_month) def _apply(self, n, other): @@ -2211,7 +2211,7 @@ def f(self, other): return f -class Tick(SingleConstructorOffset): +class Tick(SingleConstructorOffset, liboffsets._Tick): _inc = Timedelta(microseconds=1000) _prefix = 'undefined' _attributes = frozenset(['n', 'normalize']) From d3eb043656cf2d7ae644d02242596eba72b9a0bd Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 20:06:26 -0700 Subject: [PATCH 02/11] revert _Tick since it breaks pickles --- pandas/tseries/offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 7f5bd67a85ae3..a5a983bf94bb8 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2211,7 +2211,7 @@ def f(self, other): return f -class Tick(SingleConstructorOffset, liboffsets._Tick): +class Tick(SingleConstructorOffset): _inc = Timedelta(microseconds=1000) _prefix = 'undefined' _attributes = frozenset(['n', 'normalize']) From c1c62796042db9599b6c556a75fc3f96034ecb40 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:37:37 -0700 Subject: [PATCH 03/11] remove _Tick --- pandas/_libs/tslibs/offsets.pyx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index f22aef2344608..8caf9ea0e0389 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -395,13 +395,6 @@ class BaseOffset(_BaseOffset): return -self + other -cdef class _Tick(object): - # dummy class to mix in to offsets.Tick so that we can write cython - # code to check `isinstance(obj, _Tick)` instead of `isinstance(obj, Tick)`, - # i.e. avoid non-cython dependencies. - pass - - # ---------------------------------------------------------------------- # RelativeDelta Arithmetic From 2f2391f439fc4f1ebad0ab1a4d74628753ef074c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:37:59 -0700 Subject: [PATCH 04/11] remove _Tick --- pandas/_libs/tslibs/offsets.pxd | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 pandas/_libs/tslibs/offsets.pxd diff --git a/pandas/_libs/tslibs/offsets.pxd b/pandas/_libs/tslibs/offsets.pxd deleted file mode 100644 index ec89e61aa867b..0000000000000 --- a/pandas/_libs/tslibs/offsets.pxd +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# cython: profile=False - -cdef class _Tick(object): - pass From a64a5b4ac60a74e668fbc3148b78fa499b30dae6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:50:06 -0700 Subject: [PATCH 05/11] revert a bit --- pandas/_libs/tslibs/period.pyx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 6258916af4bf0..b7df1a87cbbcc 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -51,13 +51,13 @@ from conversion cimport tz_convert_utc_to_tzlocal from frequencies cimport (get_freq_code, get_base_alias, get_to_timestamp_base, get_freq_str, get_rule_month) -#from offsets cimport _Tick from parsing import parse_time_string, NAT_SENTINEL from resolution import Resolution from nattype import nat_strings, NaT, iNaT from nattype cimport _nat_scalar_rules, NPY_NAT -from pandas.tseries import frequencies, offsets +from pandas.tseries import offsets +from pandas.tseries import frequencies cdef extern from "period_helper.h": From 140f14901f5920f24610ffefd5e9eab7114dbdef Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:51:51 -0700 Subject: [PATCH 06/11] revert resolution --- pandas/_libs/tslibs/resolution.pyx | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/pandas/_libs/tslibs/resolution.pyx b/pandas/_libs/tslibs/resolution.pyx index cd81734e77c7b..d0a9501afe566 100644 --- a/pandas/_libs/tslibs/resolution.pyx +++ b/pandas/_libs/tslibs/resolution.pyx @@ -5,7 +5,7 @@ from cython cimport Py_ssize_t import numpy as np cimport numpy as cnp -from numpy cimport ndarray, int64_t, int32_t +from numpy cimport ndarray, int64_t cnp.import_array() from util cimport is_string_object, get_nat @@ -25,7 +25,6 @@ from fields import build_field_sarray from conversion import tz_convert from conversion cimport tz_convert_utc_to_tzlocal from ccalendar import MONTH_ALIASES, int_to_weekday -from ccalendar cimport get_days_in_month from pandas._libs.properties import cache_readonly from pandas._libs.tslib import Timestamp @@ -350,7 +349,7 @@ class Resolution(object): # TODO: this is non performant logic here (and duplicative) and this # simply should call unique_1d directly # plus no reason to depend on khash directly -cdef ndarray[int64_t, ndim=1] unique_deltas(ndarray[int64_t] arr): +cdef unique_deltas(ndarray[int64_t] arr): cdef: Py_ssize_t i, n = len(arr) int64_t val @@ -385,11 +384,10 @@ def _maybe_add_count(base, count): return base -cdef class _FrequencyInferer(object): +class _FrequencyInferer(object): """ Not sure if I can avoid the state machine here """ - cdef public index, values, warn, is_monotonic, _cache def __init__(self, index, warn=True): self.index = index @@ -477,20 +475,19 @@ cdef class _FrequencyInferer(object): def rep_stamp(self): return Timestamp(self.values[0]) - cdef month_position_check(self): + def month_position_check(self): # TODO: cythonize this, very slow - cdef: - int32_t daysinmonth - bint calendar_end = True - bint business_end = True - bint calendar_start = True - bint business_start = True + calendar_end = True + business_end = True + calendar_start = True + business_start = True years = self.fields['Y'] months = self.fields['M'] days = self.fields['D'] weekdays = self.index.dayofweek + from calendar import monthrange for y, m, d, wd in zip(years, months, days, weekdays): if calendar_start: @@ -499,7 +496,7 @@ cdef class _FrequencyInferer(object): business_start &= d == 1 or (d <= 3 and wd == 0) if calendar_end or business_end: - daysinmonth = get_days_in_month(y, m) + _, daysinmonth = monthrange(y, m) cal = d == daysinmonth if calendar_end: calendar_end &= cal @@ -594,7 +591,7 @@ cdef class _FrequencyInferer(object): return {'cs': 'MS', 'bs': 'BMS', 'ce': 'M', 'be': 'BM'}.get(pos_check) - cdef bint _is_business_daily(self): + def _is_business_daily(self): # quick check: cannot be business daily if self.day_deltas != [1, 3]: return False From 4752e3653ef68e004192d5cd20837d008c83a2c1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:54:22 -0700 Subject: [PATCH 07/11] remove unnecessary to_offset call --- pandas/_libs/tslibs/period.pyx | 8 ++------ pandas/tests/tseries/offsets/test_offsets.py | 6 ++++++ pandas/tseries/offsets.py | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index b7df1a87cbbcc..3c7354828ae23 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1063,13 +1063,9 @@ cdef class _Period(object): if (PyDelta_Check(other) or util.is_timedelta64_object(other) or isinstance(other, offsets.Tick)): - offset = frequencies.to_offset(self.freq.rule_code) - if isinstance(offset, offsets.Tick): + if isinstance(self.freq, offsets.Tick): nanos = delta_to_nanoseconds(other) - offset_nanos = delta_to_nanoseconds(offset) - - assert isinstance(self.freq, offsets.Tick), self.freq # pretty sure we can skip the frequencies.to_offset call - + offset_nanos = delta_to_nanoseconds(self.freq) if nanos % offset_nanos == 0: ordinal = self.ordinal + (nanos // offset_nanos) return Period(ordinal=ordinal, freq=self.freq) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index a1c5a825054ec..5369b1a94a956 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -41,6 +41,12 @@ from .common import assert_offset_equal, assert_onOffset +def test_monthrange(): + import calendar + for y in range(2000, 2013): + for m in range(1, 13): + assert tslib.monthrange(y, m) == calendar.monthrange(y, m) + #### # Misc function tests #### diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index a5a983bf94bb8..c294110d89ec5 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1140,7 +1140,7 @@ def apply(self, other): # shift `other` to self.day_of_month, incrementing `n` if necessary n = liboffsets.roll_convention(other.day, self.n, self.day_of_month) - days_in_month = ccalendar.get_days_in_month(other.year, other.month) + days_in_month = tslib.monthrange(other.year, other.month)[1] # For SemiMonthBegin on other.day == 1 and # SemiMonthEnd on other.day == days_in_month, @@ -1217,7 +1217,7 @@ class SemiMonthEnd(SemiMonthOffset): def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - days_in_month = ccalendar.get_days_in_month(dt.year, dt.month) + _, days_in_month = tslib.monthrange(dt.year, dt.month) return dt.day in (self.day_of_month, days_in_month) def _apply(self, n, other): From 3fea74557c420c52fe61a6a79d7edcdc05b0da63 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 11 Jun 2018 21:55:00 -0700 Subject: [PATCH 08/11] revert --- pandas/core/indexes/datetimes.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 66622814f172d..d44b13172f86d 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -55,7 +55,6 @@ from pandas._libs import (lib, index as libindex, tslib as libts, join as libjoin, Timestamp) from pandas._libs.tslibs import (timezones, conversion, fields, parsing, - ccalendar, resolution as libresolution) # -------- some conversion wrapper functions @@ -1452,14 +1451,14 @@ def _parsed_string_to_bounds(self, reso, parsed): Timestamp(datetime(parsed.year, 12, 31, 23, 59, 59, 999999), tz=self.tz)) elif reso == 'month': - d = ccalendar.get_days_in_month(parsed.year, parsed.month) + d = libts.monthrange(parsed.year, parsed.month)[1] return (Timestamp(datetime(parsed.year, parsed.month, 1), tz=self.tz), Timestamp(datetime(parsed.year, parsed.month, d, 23, 59, 59, 999999), tz=self.tz)) elif reso == 'quarter': qe = (((parsed.month - 1) + 2) % 12) + 1 # two months ahead - d = ccalendar.get_days_in_month(parsed.year, qe) # at end of month + d = libts.monthrange(parsed.year, qe)[1] # at end of month return (Timestamp(datetime(parsed.year, parsed.month, 1), tz=self.tz), Timestamp(datetime(parsed.year, qe, d, 23, 59, From 622bed7f43d3ae723ba02bedc0d85ef6437056f6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 12 Jun 2018 19:27:39 -0700 Subject: [PATCH 09/11] fixup wrap long line --- pandas/_libs/tslibs/period.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 3c7354828ae23..b1f46b4a4575d 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -19,7 +19,8 @@ from pandas.compat import PY2 cimport cython -from cpython.datetime cimport PyDateTime_Check, PyDelta_Check, PyDateTime_IMPORT +from cpython.datetime cimport (PyDateTime_Check, PyDelta_Check, + PyDateTime_IMPORT) # import datetime C API PyDateTime_IMPORT From daf9aaf7a574ba800984b871ac21c64446a79d25 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 12 Jun 2018 19:32:26 -0700 Subject: [PATCH 10/11] restore apparently-necessary to_offset call --- pandas/_libs/tslibs/period.pyx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index b1f46b4a4575d..fadd8baede609 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1064,9 +1064,10 @@ cdef class _Period(object): if (PyDelta_Check(other) or util.is_timedelta64_object(other) or isinstance(other, offsets.Tick)): - if isinstance(self.freq, offsets.Tick): + offset = frequencies.to_offset(self.freq.rule_code) + if isinstance(offset, offsets.Tick): nanos = delta_to_nanoseconds(other) - offset_nanos = delta_to_nanoseconds(self.freq) + offset_nanos = delta_to_nanoseconds(offset) if nanos % offset_nanos == 0: ordinal = self.ordinal + (nanos // offset_nanos) return Period(ordinal=ordinal, freq=self.freq) From a3d11c625f03bb800a5389d962d982fe526063e5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 14 Jun 2018 08:25:08 -0700 Subject: [PATCH 11/11] implement is_offset_object --- pandas/_libs/src/util.pxd | 15 +++++++++++++++ pandas/_libs/tslibs/period.pyx | 6 +++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/src/util.pxd b/pandas/_libs/src/util.pxd index d8249ec130f4d..2c1876fad95d2 100644 --- a/pandas/_libs/src/util.pxd +++ b/pandas/_libs/src/util.pxd @@ -161,3 +161,18 @@ cdef inline bint _checknull(object val): cdef inline bint is_period_object(object val): return getattr(val, '_typ', '_typ') == 'period' + + +cdef inline bint is_offset_object(object val): + """ + Check if an object is a DateOffset object. + + Parameters + ---------- + val : object + + Returns + ------- + is_date_offset : bool + """ + return getattr(val, '_typ', None) == "dateoffset" diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index fadd8baede609..cc2fb6e0617cb 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1073,7 +1073,7 @@ cdef class _Period(object): return Period(ordinal=ordinal, freq=self.freq) msg = 'Input cannot be converted to Period(freq={0})' raise IncompatibleFrequency(msg.format(self.freqstr)) - elif getattr(other, "_typ", None) == "dateoffset": + elif util.is_offset_object(other): freqstr = other.rule_code base = get_base_alias(freqstr) if base == self.freq.rule_code: @@ -1087,7 +1087,7 @@ cdef class _Period(object): def __add__(self, other): if is_period_object(self): if (PyDelta_Check(other) or util.is_timedelta64_object(other) or - getattr(other, "_typ", None) == "dateoffset"): + util.is_offset_object(other)): return self._add_delta(other) elif other is NaT: return NaT @@ -1114,7 +1114,7 @@ cdef class _Period(object): def __sub__(self, other): if is_period_object(self): if (PyDelta_Check(other) or util.is_timedelta64_object(other) or - getattr(other, "_typ", None) == "dateoffset"): + util.is_offset_object(other)): neg_other = -other return self + neg_other elif util.is_integer_object(other):