Skip to content
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.23.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ Conversion
- Bug in :class:`Series` constructor with an int or float list where specifying ``dtype=str``, ``dtype='str'`` or ``dtype='U'`` failed to convert the data elements to strings (:issue:`16605`)
- 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:`18510`,:issue:`18672`,:issue:`18864`)
- Bug in :class:`FY5253` where ``datetime`` addition and subtraction incremented incorrectly for dates on the year-end but not normalized to midnight (:issue:`18854`)


Indexing
Expand Down
30 changes: 19 additions & 11 deletions pandas/tests/tseries/offsets/test_fiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,6 @@ def test_apply(self):

class TestFY5253NearestEndMonth(Base):

def test_get_target_month_end(self):
assert (makeFY5253NearestEndMonth(
Copy link
Contributor

Choose a reason for hiding this comment

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

are these tests wrong?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, but get_target_month_end is removed in this PR.

startingMonth=8, weekday=WeekDay.SAT).get_target_month_end(
datetime(2013, 1, 1)) == datetime(2013, 8, 31))
assert (makeFY5253NearestEndMonth(
startingMonth=12, weekday=WeekDay.SAT).get_target_month_end(
datetime(2013, 1, 1)) == datetime(2013, 12, 31))
assert (makeFY5253NearestEndMonth(
startingMonth=2, weekday=WeekDay.SAT).get_target_month_end(
datetime(2013, 1, 1)) == datetime(2013, 2, 28))

def test_get_year_end(self):
assert (makeFY5253NearestEndMonth(
startingMonth=8, weekday=WeekDay.SAT).get_year_end(
Expand Down Expand Up @@ -625,3 +614,22 @@ def test_bunched_yearends():
assert fy.rollback(dt) == Timestamp('2002-12-28')
assert (-fy).apply(dt) == Timestamp('2002-12-28')
assert dt - fy == Timestamp('2002-12-28')


def test_fy5253_last_onoffset():
# GH#18877 dates on the year-end but not normalized to midnight
offset = FY5253(n=-5, startingMonth=5, variation="last", weekday=0)
Copy link
Contributor

Choose a reason for hiding this comment

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

add a comment to these tests about what you are testing. next person to come along will have no idea. add the issue number as well.

ts = Timestamp('1984-05-28 06:29:43.955911354+0200',
tz='Europe/San_Marino')
fast = offset.onOffset(ts)
slow = (ts + offset) - offset == ts
assert fast == slow


def test_fy5253_nearest_onoffset():
# GH#18877 dates on the year-end but not normalized to midnight
offset = FY5253(n=3, startingMonth=7, variation="nearest", weekday=2)
ts = Timestamp('2032-07-28 00:12:59.035729419+0000', tz='Africa/Dakar')
fast = offset.onOffset(ts)
slow = (ts + offset) - offset == ts
assert fast == slow
78 changes: 31 additions & 47 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -1814,13 +1814,6 @@ def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
raise ValueError('{variation} is not a valid variation'
.format(variation=self.variation))

@cache_readonly
def _offset_lwom(self):
if self.variation == "nearest":
return None
else:
return LastWeekOfMonth(n=1, weekday=self.weekday)

def isAnchored(self):
return (self.n == 1 and
self.startingMonth is not None and
Expand All @@ -1841,6 +1834,8 @@ def onOffset(self, dt):

@apply_wraps
def apply(self, other):
norm = Timestamp(other).normalize()

n = self.n
prev_year = self.get_year_end(
datetime(other.year - 1, self.startingMonth, 1))
Expand All @@ -1853,32 +1848,26 @@ def apply(self, other):
cur_year = tslib._localize_pydatetime(cur_year, other.tzinfo)
next_year = tslib._localize_pydatetime(next_year, other.tzinfo)

if other == prev_year:
# Note: next_year.year == other.year + 1, so we will always
Copy link
Contributor

Choose a reason for hiding this comment

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

pls add a blank line (generally) before comments.

# have other < next_year
if norm == prev_year:
n -= 1
elif other == cur_year:
elif norm == cur_year:
pass
elif other == next_year:
n += 1
# TODO: Not hit in tests
Copy link
Member Author

Choose a reason for hiding this comment

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

Double-checked this, it cannot be reached. See new comment above re next_year.year.

elif n > 0:
if other < prev_year:
if norm < prev_year:
n -= 2
elif prev_year < other < cur_year:
elif prev_year < norm < cur_year:
n -= 1
elif cur_year < other < next_year:
elif cur_year < norm < next_year:
pass
else:
assert False
Copy link
Member Author

Choose a reason for hiding this comment

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

Double-checked this, it cannot be reached. See new comment above re next_year.year.

else:
if next_year < other:
n += 2
# TODO: Not hit in tests; UPDATE: looks impossible
Copy link
Member Author

Choose a reason for hiding this comment

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

Double-checked this, it cannot be reached. See new comment above re next_year.year.

elif cur_year < other < next_year:
if cur_year < norm < next_year:
n += 1
elif prev_year < other < cur_year:
elif prev_year < norm < cur_year:
pass
elif (other.year == prev_year.year and other < prev_year and
prev_year - other <= timedelta(6)):
elif (norm.year == prev_year.year and norm < prev_year and
prev_year - norm <= timedelta(6)):
# GH#14774, error when next_year.year == cur_year.year
# e.g. prev_year == datetime(2004, 1, 3),
# other == datetime(2004, 1, 1)
Expand All @@ -1894,35 +1883,30 @@ def apply(self, other):
return result

def get_year_end(self, dt):
if self.variation == "nearest":
return self._get_year_end_nearest(dt)
else:
return self._get_year_end_last(dt)

def get_target_month_end(self, dt):
target_month = datetime(dt.year, self.startingMonth, 1,
tzinfo=dt.tzinfo)
return shift_month(target_month, 0, 'end')
# TODO: is this DST-safe?
Copy link
Member Author

@jbrockmendel jbrockmendel Dec 20, 2017

Choose a reason for hiding this comment

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

non-issue b/c get_target_month_end is only ever called with dt.tzinfo is None

assert dt.tzinfo is None

def _get_year_end_nearest(self, dt):
target_date = self.get_target_month_end(dt)
dim = ccalendar.get_days_in_month(dt.year, self.startingMonth)
target_date = datetime(dt.year, self.startingMonth, dim)
wkday_diff = self.weekday - target_date.weekday()
if wkday_diff == 0:
# year_end is the same for "last" and "nearest" cases
return target_date

days_forward = wkday_diff % 7
if days_forward <= 3:
# The upcoming self.weekday is closer than the previous one
return target_date + timedelta(days_forward)
else:
# The previous self.weekday is closer than the upcoming one
return target_date + timedelta(days_forward - 7)
if self.variation == "last":
days_forward = (wkday_diff % 7) - 7

def _get_year_end_last(self, dt):
current_year = datetime(dt.year, self.startingMonth, 1,
tzinfo=dt.tzinfo)
Copy link
Member Author

Choose a reason for hiding this comment

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

only called with dt.tzinfo is None

return current_year + self._offset_lwom
# days_forward is always negative, so we always end up
Copy link
Contributor

Choose a reason for hiding this comment

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

here

# in the same year as dt
return target_date + timedelta(days=days_forward)
else:
# variation == "nearest":
days_forward = wkday_diff % 7
if days_forward <= 3:
# The upcoming self.weekday is closer than the previous one
Copy link
Contributor

Choose a reason for hiding this comment

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

these comments are good, FYI (you only need a blank line when the indentation is the same)

return target_date + timedelta(days_forward)
else:
# The previous self.weekday is closer than the upcoming one
return target_date + timedelta(days_forward - 7)

@property
def rule_code(self):
Expand Down