Skip to content

simplify+unify offset.apply logic #18263

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Nov 16, 2017
Merged
Show file tree
Hide file tree
Changes from 2 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
31 changes: 23 additions & 8 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,21 @@ def apply_index_wraps(func):
# ---------------------------------------------------------------------
# Business Helpers

cpdef int _get_firstbday(int wkday):
cpdef int _get_lastbday(int wkday, int days_in_month):
"""
wkday is the result of monthrange(year, month)
(wkday, days_in_month) is the result of monthrange(year, month)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you update doc-strings

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update to reflect what?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean update to a more proper doc-string

"""
return days_in_month - max(((wkday + days_in_month - 1) % 7) - 4, 0)


cpdef int _get_firstbday(int wkday, int days_in_month=0):
"""
(wkday, days_in_month) is the result of monthrange(year, month)

If it's a saturday or sunday, increment first business day to reflect this

days_in_month arg is a dummy so that this has the same signature as
_get_lastbday.
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can type these

first = 1
if wkday == 5: # on Saturday
Expand Down Expand Up @@ -380,7 +390,6 @@ class BaseOffset(_BaseOffset):
# ----------------------------------------------------------------------
# RelativeDelta Arithmetic


cpdef datetime shift_month(datetime stamp, int months, object day_opt=None):
"""
Given a datetime (or Timestamp) `stamp`, an integer `months` and an
Expand All @@ -406,7 +415,7 @@ cpdef datetime shift_month(datetime stamp, int months, object day_opt=None):
"""
cdef:
int year, month, day
int dim, dy
int wkday, days_in_month, dy

dy = (stamp.month + months) // 12
month = (stamp.month + months) % 12
Expand All @@ -416,15 +425,21 @@ cpdef datetime shift_month(datetime stamp, int months, object day_opt=None):
dy -= 1
year = stamp.year + dy

dim = monthrange(year, month)[1]
(wkday, days_in_month) = monthrange(year, month)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no parens

if day_opt is None:
day = min(stamp.day, dim)
day = min(stamp.day, days_in_month)
elif day_opt == 'start':
day = 1
elif day_opt == 'end':
day = dim
day = days_in_month
elif day_opt == 'bstart':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about business_start/end

# first business day of month
day = _get_firstbday(wkday, days_in_month)
elif day_opt == 'bend':
# last business day of month
day = _get_lastbday(wkday, days_in_month)
elif is_integer_object(day_opt):
day = min(day_opt, dim)
day = min(day_opt, days_in_month)
else:
raise ValueError(day_opt)
return stamp.replace(year=year, month=month, day=day)
75 changes: 14 additions & 61 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from pandas._libs.tslibs.offsets import (
ApplyTypeError,
as_datetime, _is_normalized,
_get_firstbday, _get_calendar, _to_dt64, _validate_business_time,
_get_firstbday, _get_lastbday,
_get_calendar, _to_dt64, _validate_business_time,
_int_to_weekday, _weekday_to_int,
_determine_offset,
apply_index_wraps,
Expand Down Expand Up @@ -1188,18 +1189,14 @@ class BusinessMonthEnd(MonthOffset):
def apply(self, other):
n = self.n
wkday, days_in_month = tslib.monthrange(other.year, other.month)
lastBDay = days_in_month - max(((wkday + days_in_month - 1)
% 7) - 4, 0)
lastBDay = _get_lastbday(wkday, days_in_month)

if n > 0 and not other.day >= lastBDay:
n = n - 1
elif n <= 0 and other.day > lastBDay:
n = n + 1
other = shift_month(other, n, 'end')

if other.weekday() > 4:
other = other - BDay()
return other
return shift_month(other, n, 'bend')


class BusinessMonthBegin(MonthOffset):
Expand All @@ -1219,24 +1216,13 @@ def apply(self, other):
other = other + timedelta(days=first - other.day)
n -= 1

other = shift_month(other, n, None)
wkday, _ = tslib.monthrange(other.year, other.month)
first = _get_firstbday(wkday)
result = datetime(other.year, other.month, first,
other.hour, other.minute,
other.second, other.microsecond)
return result
return shift_month(other, n, 'bstart')

def onOffset(self, dt):
if self.normalize and not _is_normalized(dt):
return False
first_weekday, _ = tslib.monthrange(dt.year, dt.month)
if first_weekday == 5:
return dt.day == 3
elif first_weekday == 6:
return dt.day == 2
else:
return dt.day == 1
return dt.day == _get_firstbday(first_weekday)


class CustomBusinessMonthEnd(BusinessMixin, MonthOffset):
Expand Down Expand Up @@ -1648,10 +1634,7 @@ def _from_name(cls, suffix=None):

class QuarterOffset(DateOffset):
"""Quarter representation - doesn't call super"""

#: default month for __init__
_default_startingMonth = None
#: default month in _from_name
_from_name_startingMonth = None
_adjust_dst = True
# TODO: Consider combining QuarterOffset and YearOffset __init__ at some
Expand Down Expand Up @@ -1693,21 +1676,15 @@ class BQuarterEnd(QuarterOffset):
"""
_outputName = 'BusinessQuarterEnd'
_default_startingMonth = 3
# 'BQ'
_from_name_startingMonth = 12
_prefix = 'BQ'

@apply_wraps
def apply(self, other):
n = self.n
base = other
other = datetime(other.year, other.month, other.day,
other.hour, other.minute, other.second,
other.microsecond)

wkday, days_in_month = tslib.monthrange(other.year, other.month)
lastBDay = days_in_month - max(((wkday + days_in_month - 1)
% 7) - 4, 0)
lastBDay = _get_lastbday(wkday, days_in_month)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok to de-privatize get_lastbday/first


monthsToGo = 3 - ((other.month - self.startingMonth) % 3)
if monthsToGo == 3:
Expand All @@ -1718,11 +1695,7 @@ def apply(self, other):
elif n <= 0 and other.day > lastBDay and monthsToGo == 0:
n = n + 1

other = shift_month(other, monthsToGo + 3 * n, 'end')
other = tslib._localize_pydatetime(other, base.tzinfo)
if other.weekday() > 4:
other = other - BDay()
return other
return shift_month(other, monthsToGo + 3 * n, 'bend')

def onOffset(self, dt):
if self.normalize and not _is_normalized(dt):
Expand Down Expand Up @@ -1762,14 +1735,7 @@ def apply(self, other):
elif n > 0 and (monthsSince == 0 and other.day < first):
n = n - 1

# get the first bday for result
other = shift_month(other, 3 * n - monthsSince, None)
wkday, _ = tslib.monthrange(other.year, other.month)
first = _get_firstbday(wkday)
result = datetime(other.year, other.month, first,
other.hour, other.minute, other.second,
other.microsecond)
return result
return shift_month(other, 3 * n - monthsSince, 'bstart')


class QuarterEnd(EndMixin, QuarterOffset):
Expand Down Expand Up @@ -1878,8 +1844,7 @@ class BYearEnd(YearOffset):
def apply(self, other):
n = self.n
wkday, days_in_month = tslib.monthrange(other.year, self.month)
lastBDay = (days_in_month -
max(((wkday + days_in_month - 1) % 7) - 4, 0))
lastBDay = _get_lastbday(wkday, days_in_month)

years = n
if n > 0:
Expand All @@ -1891,17 +1856,8 @@ def apply(self, other):
(other.month == self.month and other.day > lastBDay)):
years += 1

other = shift_month(other, 12 * years, None)

_, days_in_month = tslib.monthrange(other.year, self.month)
result = datetime(other.year, self.month, days_in_month,
other.hour, other.minute, other.second,
other.microsecond)

if result.weekday() > 4:
result = result - BDay()

return result
months = years * 12 + (self.month - other.month)
return shift_month(other, months, 'bend')


class BYearBegin(YearOffset):
Expand Down Expand Up @@ -1929,11 +1885,8 @@ def apply(self, other):
years += 1

# set first bday for result
other = shift_month(other, years * 12, None)
wkday, days_in_month = tslib.monthrange(other.year, self.month)
first = _get_firstbday(wkday)
return datetime(other.year, self.month, first, other.hour,
other.minute, other.second, other.microsecond)
months = years * 12 + (self.month - other.month)
return shift_month(other, months, 'bstart')


class YearEnd(EndMixin, YearOffset):
Expand Down