-
-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Fix bugs in WeekOfMonth.apply, Week.onOffset #18875
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
Changes from 2 commits
a35c10b
07cb502
2dc8ad1
40ff136
e71374f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -362,3 +362,4 @@ Other | |
|
||
- Improved error message when attempting to use a Python keyword as an identifier in a ``numexpr`` backed query (:issue:`18221`) | ||
- Bug in :class:`Timestamp` where comparison with an array of ``Timestamp`` objects would result in a ``RecursionError`` (:issue:`15183`) | ||
- Bug in :class:`WeekOfMonth` and class:`Week` where addition and subtraction did not roll correctly (:issue:`18672`,:issue:`18510`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there is a 3rd issue no? |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -112,6 +112,31 @@ def wrapper(self, other): | |
return wrapper | ||
|
||
|
||
def shift_day(other, days): | ||
""" | ||
Increment the datetime `other` by the given number of days, retaining | ||
the time-portion of the datetime. For tz-naive datetimes this is | ||
equivalent to adding a timedelta. For tz-aware datetimes it is similar to | ||
dateutil's relativedelta.__add__, but handles pytz tzinfo objects. | ||
|
||
Parameters | ||
---------- | ||
other : datetime or Timestamp | ||
days : int | ||
|
||
Returns | ||
------- | ||
shifted: datetime or Timestamp | ||
""" | ||
if other.tzinfo is None: | ||
return other + timedelta(days=days) | ||
|
||
tz = other.tzinfo | ||
naive = other.replace(tzinfo=None) | ||
shifted = naive + timedelta(days=days) | ||
return tslib._localize_pydatetime(shifted, tz) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. side issue. |
||
|
||
|
||
# --------------------------------------------------------------------- | ||
# DateOffset | ||
|
||
|
@@ -1342,6 +1367,8 @@ def apply_index(self, i): | |
def onOffset(self, dt): | ||
if self.normalize and not _is_normalized(dt): | ||
return False | ||
elif self.weekday is None: | ||
return True | ||
return dt.weekday() == self.weekday | ||
|
||
@property | ||
|
@@ -1361,7 +1388,29 @@ def _from_name(cls, suffix=None): | |
return cls(weekday=weekday) | ||
|
||
|
||
class WeekOfMonth(DateOffset): | ||
class _WeekOfMonthMixin(object): | ||
"""Mixin for methods common to WeekOfMonth and LastWeekOfMonth""" | ||
@apply_wraps | ||
def apply(self, other): | ||
compare_day = self._get_offset_day(other) | ||
|
||
months = self.n | ||
if months > 0 and compare_day > other.day: | ||
months -= 1 | ||
elif months <= 0 and compare_day < other.day: | ||
months += 1 | ||
|
||
shifted = shift_month(other, months, 'start') | ||
to_day = self._get_offset_day(shifted) | ||
return shift_day(shifted, to_day - shifted.day) | ||
|
||
def onOffset(self, dt): | ||
if self.normalize and not _is_normalized(dt): | ||
return False | ||
return dt.day == self._get_offset_day(dt) | ||
|
||
|
||
class WeekOfMonth(_WeekOfMonthMixin, DateOffset): | ||
""" | ||
Describes monthly dates like "the Tuesday of the 2nd week of each month" | ||
|
||
|
@@ -1400,34 +1449,11 @@ def __init__(self, n=1, normalize=False, week=None, weekday=None): | |
|
||
self.kwds = {'weekday': weekday, 'week': week} | ||
|
||
@apply_wraps | ||
def apply(self, other): | ||
base = other | ||
offsetOfMonth = self.getOffsetOfMonth(other) | ||
|
||
months = self.n | ||
if months > 0 and offsetOfMonth > other: | ||
months -= 1 | ||
elif months <= 0 and offsetOfMonth < other: | ||
months += 1 | ||
|
||
other = self.getOffsetOfMonth(shift_month(other, months, 'start')) | ||
other = datetime(other.year, other.month, other.day, base.hour, | ||
base.minute, base.second, base.microsecond) | ||
return 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) | ||
|
||
def onOffset(self, dt): | ||
if self.normalize and not _is_normalized(dt): | ||
return False | ||
d = datetime(dt.year, dt.month, dt.day, tzinfo=dt.tzinfo) | ||
return d == self.getOffsetOfMonth(dt) | ||
def _get_offset_day(self, other): | ||
mstart = datetime(other.year, other.month, 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you document |
||
wday = mstart.weekday() | ||
shift_days = (self.weekday - wday) % 7 | ||
return 1 + shift_days + self.week * 7 | ||
|
||
@property | ||
def rule_code(self): | ||
|
@@ -1448,7 +1474,7 @@ def _from_name(cls, suffix=None): | |
return cls(week=week, weekday=weekday) | ||
|
||
|
||
class LastWeekOfMonth(DateOffset): | ||
class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset): | ||
""" | ||
Describes monthly dates in last week of month like "the last Tuesday of | ||
each month" | ||
|
@@ -1482,31 +1508,12 @@ def __init__(self, n=1, normalize=False, weekday=None): | |
|
||
self.kwds = {'weekday': weekday} | ||
|
||
@apply_wraps | ||
def apply(self, other): | ||
offsetOfMonth = self.getOffsetOfMonth(other) | ||
|
||
months = self.n | ||
if months > 0 and offsetOfMonth > other: | ||
months -= 1 | ||
elif months <= 0 and offsetOfMonth < other: | ||
months += 1 | ||
|
||
return self.getOffsetOfMonth(shift_month(other, months, 'start')) | ||
|
||
def getOffsetOfMonth(self, dt): | ||
m = MonthEnd() | ||
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) | ||
|
||
def onOffset(self, dt): | ||
if self.normalize and not _is_normalized(dt): | ||
return False | ||
return dt == self.getOffsetOfMonth(dt) | ||
def _get_offset_day(self, other): | ||
dim = ccalendar.get_days_in_month(other.year, other.month) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same |
||
mend = datetime(other.year, other.month, dim) | ||
wday = mend.weekday() | ||
shift_days = (wday - self.weekday) % 7 | ||
return dim - shift_days | ||
|
||
@property | ||
def rule_code(self): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls move both this and the one higher to conversion. pls dont' put things in Other that fit elsewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Out of curiosity, what is the heuristic that gets this lumped into "conversion"?