-
-
Notifications
You must be signed in to change notification settings - Fork 18.5k
Fix bugs in FY5253Quarter and LastWeekOfMonth #19036
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 4 commits
93c96d4
844330e
f962845
4fb19ba
cfe3341
e751df9
8b0c604
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 |
---|---|---|
|
@@ -1468,6 +1468,7 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset): | |
|
||
""" | ||
_prefix = 'LWOM' | ||
_adjust_dst = True | ||
|
||
def __init__(self, n=1, normalize=False, weekday=None): | ||
self.n = self._validate_n(n) | ||
|
@@ -1715,10 +1716,7 @@ class YearBegin(YearOffset): | |
# --------------------------------------------------------------------- | ||
# Special Offset Classes | ||
|
||
class FY5253(DateOffset): | ||
""" | ||
Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar. | ||
|
||
_5253doc = """ | ||
It is used by companies that desire that their | ||
fiscal year always end on the same day of the week. | ||
|
||
|
@@ -1727,15 +1725,22 @@ class FY5253(DateOffset): | |
such as retail, manufacturing and parking industry. | ||
|
||
For more information see: | ||
http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar | ||
|
||
http://en.wikipedia.org/wiki/4-4-5_calendar | ||
|
||
The year may either: | ||
- end on the last X day of the Y month. | ||
- end on the last X day closest to the last day of the Y month. | ||
|
||
X is a specific day of the week. | ||
Y is a certain month of the year | ||
""" | ||
|
||
|
||
class FY5253(DateOffset): | ||
__doc__ = """ | ||
Describes 52-53 week fiscal year. This is also known as a 4-4-5 calendar. | ||
|
||
%s | ||
|
||
Parameters | ||
---------- | ||
|
@@ -1751,7 +1756,7 @@ class FY5253(DateOffset): | |
startingMonth : The month in which fiscal years end. {1, 2, ... 12} | ||
variation : str | ||
{"nearest", "last"} for "LastOfMonth" or "NearestEndMonth" | ||
""" | ||
""" % _5253doc | ||
_prefix = 'RE' | ||
_adjust_dst = True | ||
|
||
|
@@ -1910,26 +1915,11 @@ def _from_name(cls, *args): | |
|
||
|
||
class FY5253Quarter(DateOffset): | ||
""" | ||
__doc__ = """ | ||
DateOffset increments between business quarter dates | ||
for 52-53 week fiscal year (also known as a 4-4-5 calendar). | ||
|
||
It is used by companies that desire that their | ||
fiscal year always end on the same day of the week. | ||
|
||
It is a method of managing accounting periods. | ||
It is a common calendar structure for some industries, | ||
such as retail, manufacturing and parking industry. | ||
|
||
For more information see: | ||
http://en.wikipedia.org/wiki/4%E2%80%934%E2%80%935_calendar | ||
|
||
The year may either: | ||
- end on the last X day of the Y month. | ||
- end on the last X day closest to the last day of the Y month. | ||
|
||
X is a specific day of the week. | ||
Y is a certain month of the year | ||
%s | ||
|
||
startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... | ||
startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... | ||
|
@@ -1951,7 +1941,7 @@ class FY5253Quarter(DateOffset): | |
or 14 week when needed. {1,2,3,4} | ||
variation : str | ||
{"nearest", "last"} for "LastOfMonth" or "NearestEndMonth" | ||
""" | ||
""" % _5253doc | ||
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 use 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. and same on all the templates (generally we want to always use the .format()) 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. That was the first attempt. That syntax doesn't play nicely with the set syntax inside the docstring. The docstring change here is tangential to the PR; I'll just revert it. |
||
|
||
_prefix = 'REQ' | ||
_adjust_dst = True | ||
|
@@ -1982,46 +1972,76 @@ def _offset(self): | |
def isAnchored(self): | ||
return self.n == 1 and self._offset.isAnchored() | ||
|
||
def _rollback_to_year(self, other): | ||
"""roll `other` back to the most recent date that was on a fiscal year | ||
end. Return the date of that year-end, the number of full quarters | ||
elapsed between that year-end and other, and the remaining Timedelta | ||
since the most recent quarter-end. | ||
|
||
Parameters | ||
---------- | ||
other : datetime or Timestamp | ||
|
||
Returns | ||
------- | ||
prev_year_end : Timestamp giving most recent fiscal year end | ||
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. tuple of |
||
num_qtrs : int | ||
tdelta : Timedelta | ||
""" | ||
num_qtrs = 0 | ||
|
||
norm = Timestamp(other).tz_localize(None) | ||
start = self._offset.rollback(norm) | ||
# Note: start <= norm and self._offset.onOffset(start) | ||
|
||
if start < norm: | ||
# roll adjustment | ||
qtr_lens = self.get_weeks(norm) | ||
|
||
# check thet qtr_lens is consistent with self._offset addition | ||
end = shift_day(start, days=7 * sum(qtr_lens)) | ||
assert self._offset.onOffset(end), (start, end, qtr_lens) | ||
|
||
tdelta = norm - start | ||
for qlen in qtr_lens: | ||
if qlen * 7 <= tdelta.days: | ||
num_qtrs += 1 | ||
tdelta -= Timedelta(days=qlen * 7) | ||
else: | ||
break | ||
else: | ||
tdelta = Timedelta(0) | ||
|
||
# Note: we always have tdelta.value >= 0 | ||
return start, num_qtrs, tdelta | ||
|
||
@apply_wraps | ||
def apply(self, other): | ||
base = other | ||
# Note: self.n == 0 is not allowed. | ||
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. you allow this below 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. This is enforced in init. |
||
n = self.n | ||
|
||
if n > 0: | ||
while n > 0: | ||
if not self._offset.onOffset(other): | ||
qtr_lens = self.get_weeks(other) | ||
start = other - self._offset | ||
else: | ||
start = other | ||
qtr_lens = self.get_weeks(other + self._offset) | ||
prev_year_end, num_qtrs, tdelta = self._rollback_to_year(other) | ||
res = prev_year_end | ||
n += num_qtrs | ||
if self.n <= 0 and tdelta.value > 0: | ||
n += 1 | ||
|
||
for weeks in qtr_lens: | ||
start += timedelta(weeks=weeks) | ||
if start > other: | ||
other = start | ||
n -= 1 | ||
break | ||
# Possible speedup by handling years first. | ||
years = n // 4 | ||
if years: | ||
res += self._offset * years | ||
n -= years * 4 | ||
|
||
else: | ||
n = -n | ||
while n > 0: | ||
if not self._offset.onOffset(other): | ||
qtr_lens = self.get_weeks(other) | ||
end = other + self._offset | ||
else: | ||
end = other | ||
qtr_lens = self.get_weeks(other) | ||
|
||
for weeks in reversed(qtr_lens): | ||
end -= timedelta(weeks=weeks) | ||
if end < other: | ||
other = end | ||
n -= 1 | ||
break | ||
other = datetime(other.year, other.month, other.day, | ||
base.hour, base.minute, base.second, base.microsecond) | ||
return other | ||
# Add an extra day to make *sure* we are getting the quarter lengths | ||
# for the upcoming year, not the previous year | ||
qtr_lens = self.get_weeks(res + Timedelta(days=1)) | ||
|
||
# Note: we always have 0 <= n < 4 | ||
weeks = sum(qtr_lens[:n]) | ||
if weeks: | ||
res = shift_day(res, days=weeks * 7) | ||
|
||
return res | ||
|
||
def get_weeks(self, dt): | ||
ret = [13] * 4 | ||
|
@@ -2034,16 +2054,15 @@ def get_weeks(self, dt): | |
return ret | ||
|
||
def year_has_extra_week(self, dt): | ||
if self._offset.onOffset(dt): | ||
prev_year_end = dt - self._offset | ||
next_year_end = dt | ||
else: | ||
next_year_end = dt + self._offset | ||
prev_year_end = dt - self._offset | ||
|
||
week_in_year = (next_year_end - prev_year_end).days / 7 | ||
# Avoid round-down errors --> normalize to get | ||
# e.g. '370D' instead of '360D23H' | ||
norm = Timestamp(dt).normalize().tz_localize(None) | ||
|
||
return week_in_year == 53 | ||
next_year_end = self._offset.rollforward(norm) | ||
prev_year_end = norm - self._offset | ||
weeks_in_year = (next_year_end - prev_year_end).days / 7 | ||
assert weeks_in_year in [52, 53], weeks_in_year | ||
return weeks_in_year == 53 | ||
|
||
def onOffset(self, dt): | ||
if self.normalize and not _is_normalized(dt): | ||
|
@@ -2056,8 +2075,8 @@ def onOffset(self, dt): | |
qtr_lens = self.get_weeks(dt) | ||
|
||
current = next_year_end | ||
for qtr_len in qtr_lens[0:4]: | ||
current += timedelta(weeks=qtr_len) | ||
for qtr_len in qtr_lens: | ||
current = shift_day(current, days=qtr_len * 7) | ||
if dt == current: | ||
return True | ||
return False | ||
|
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.
Reference your PR as a comment. Same for all other tests that you added.