Skip to content

Commit f42ae78

Browse files
jbrockmendeljreback
authored andcommitted
Fix FY5253 onOffset/apply bug, simplify (pandas-dev#18877)
1 parent e1b638e commit f42ae78

File tree

3 files changed

+51
-58
lines changed

3 files changed

+51
-58
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ Conversion
290290
- Bug in :class:`Timestamp` where comparison with an array of ``Timestamp`` objects would result in a ``RecursionError`` (:issue:`15183`)
291291
- Bug in :class:`WeekOfMonth` and class:`Week` where addition and subtraction did not roll correctly (:issue:`18510`,:issue:`18672`,:issue:`18864`)
292292
- Bug in :meth:`DatetimeIndex.astype` when converting between timezone aware dtypes, and converting from timezone aware to naive (:issue:`18951`)
293+
- Bug in :class:`FY5253` where ``datetime`` addition and subtraction incremented incorrectly for dates on the year-end but not normalized to midnight (:issue:`18854`)
293294

294295

295296
Indexing

pandas/tests/tseries/offsets/test_fiscal.py

+19-11
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,6 @@ def test_apply(self):
158158

159159
class TestFY5253NearestEndMonth(Base):
160160

161-
def test_get_target_month_end(self):
162-
assert (makeFY5253NearestEndMonth(
163-
startingMonth=8, weekday=WeekDay.SAT).get_target_month_end(
164-
datetime(2013, 1, 1)) == datetime(2013, 8, 31))
165-
assert (makeFY5253NearestEndMonth(
166-
startingMonth=12, weekday=WeekDay.SAT).get_target_month_end(
167-
datetime(2013, 1, 1)) == datetime(2013, 12, 31))
168-
assert (makeFY5253NearestEndMonth(
169-
startingMonth=2, weekday=WeekDay.SAT).get_target_month_end(
170-
datetime(2013, 1, 1)) == datetime(2013, 2, 28))
171-
172161
def test_get_year_end(self):
173162
assert (makeFY5253NearestEndMonth(
174163
startingMonth=8, weekday=WeekDay.SAT).get_year_end(
@@ -625,3 +614,22 @@ def test_bunched_yearends():
625614
assert fy.rollback(dt) == Timestamp('2002-12-28')
626615
assert (-fy).apply(dt) == Timestamp('2002-12-28')
627616
assert dt - fy == Timestamp('2002-12-28')
617+
618+
619+
def test_fy5253_last_onoffset():
620+
# GH#18877 dates on the year-end but not normalized to midnight
621+
offset = FY5253(n=-5, startingMonth=5, variation="last", weekday=0)
622+
ts = Timestamp('1984-05-28 06:29:43.955911354+0200',
623+
tz='Europe/San_Marino')
624+
fast = offset.onOffset(ts)
625+
slow = (ts + offset) - offset == ts
626+
assert fast == slow
627+
628+
629+
def test_fy5253_nearest_onoffset():
630+
# GH#18877 dates on the year-end but not normalized to midnight
631+
offset = FY5253(n=3, startingMonth=7, variation="nearest", weekday=2)
632+
ts = Timestamp('2032-07-28 00:12:59.035729419+0000', tz='Africa/Dakar')
633+
fast = offset.onOffset(ts)
634+
slow = (ts + offset) - offset == ts
635+
assert fast == slow

pandas/tseries/offsets.py

+31-47
Original file line numberDiff line numberDiff line change
@@ -1814,13 +1814,6 @@ def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
18141814
raise ValueError('{variation} is not a valid variation'
18151815
.format(variation=self.variation))
18161816

1817-
@cache_readonly
1818-
def _offset_lwom(self):
1819-
if self.variation == "nearest":
1820-
return None
1821-
else:
1822-
return LastWeekOfMonth(n=1, weekday=self.weekday)
1823-
18241817
def isAnchored(self):
18251818
return (self.n == 1 and
18261819
self.startingMonth is not None and
@@ -1841,6 +1834,8 @@ def onOffset(self, dt):
18411834

18421835
@apply_wraps
18431836
def apply(self, other):
1837+
norm = Timestamp(other).normalize()
1838+
18441839
n = self.n
18451840
prev_year = self.get_year_end(
18461841
datetime(other.year - 1, self.startingMonth, 1))
@@ -1853,32 +1848,26 @@ def apply(self, other):
18531848
cur_year = tslib._localize_pydatetime(cur_year, other.tzinfo)
18541849
next_year = tslib._localize_pydatetime(next_year, other.tzinfo)
18551850

1856-
if other == prev_year:
1851+
# Note: next_year.year == other.year + 1, so we will always
1852+
# have other < next_year
1853+
if norm == prev_year:
18571854
n -= 1
1858-
elif other == cur_year:
1855+
elif norm == cur_year:
18591856
pass
1860-
elif other == next_year:
1861-
n += 1
1862-
# TODO: Not hit in tests
18631857
elif n > 0:
1864-
if other < prev_year:
1858+
if norm < prev_year:
18651859
n -= 2
1866-
elif prev_year < other < cur_year:
1860+
elif prev_year < norm < cur_year:
18671861
n -= 1
1868-
elif cur_year < other < next_year:
1862+
elif cur_year < norm < next_year:
18691863
pass
1870-
else:
1871-
assert False
18721864
else:
1873-
if next_year < other:
1874-
n += 2
1875-
# TODO: Not hit in tests; UPDATE: looks impossible
1876-
elif cur_year < other < next_year:
1865+
if cur_year < norm < next_year:
18771866
n += 1
1878-
elif prev_year < other < cur_year:
1867+
elif prev_year < norm < cur_year:
18791868
pass
1880-
elif (other.year == prev_year.year and other < prev_year and
1881-
prev_year - other <= timedelta(6)):
1869+
elif (norm.year == prev_year.year and norm < prev_year and
1870+
prev_year - norm <= timedelta(6)):
18821871
# GH#14774, error when next_year.year == cur_year.year
18831872
# e.g. prev_year == datetime(2004, 1, 3),
18841873
# other == datetime(2004, 1, 1)
@@ -1894,35 +1883,30 @@ def apply(self, other):
18941883
return result
18951884

18961885
def get_year_end(self, dt):
1897-
if self.variation == "nearest":
1898-
return self._get_year_end_nearest(dt)
1899-
else:
1900-
return self._get_year_end_last(dt)
1901-
1902-
def get_target_month_end(self, dt):
1903-
target_month = datetime(dt.year, self.startingMonth, 1,
1904-
tzinfo=dt.tzinfo)
1905-
return shift_month(target_month, 0, 'end')
1906-
# TODO: is this DST-safe?
1886+
assert dt.tzinfo is None
19071887

1908-
def _get_year_end_nearest(self, dt):
1909-
target_date = self.get_target_month_end(dt)
1888+
dim = ccalendar.get_days_in_month(dt.year, self.startingMonth)
1889+
target_date = datetime(dt.year, self.startingMonth, dim)
19101890
wkday_diff = self.weekday - target_date.weekday()
19111891
if wkday_diff == 0:
1892+
# year_end is the same for "last" and "nearest" cases
19121893
return target_date
19131894

1914-
days_forward = wkday_diff % 7
1915-
if days_forward <= 3:
1916-
# The upcoming self.weekday is closer than the previous one
1917-
return target_date + timedelta(days_forward)
1918-
else:
1919-
# The previous self.weekday is closer than the upcoming one
1920-
return target_date + timedelta(days_forward - 7)
1895+
if self.variation == "last":
1896+
days_forward = (wkday_diff % 7) - 7
19211897

1922-
def _get_year_end_last(self, dt):
1923-
current_year = datetime(dt.year, self.startingMonth, 1,
1924-
tzinfo=dt.tzinfo)
1925-
return current_year + self._offset_lwom
1898+
# days_forward is always negative, so we always end up
1899+
# in the same year as dt
1900+
return target_date + timedelta(days=days_forward)
1901+
else:
1902+
# variation == "nearest":
1903+
days_forward = wkday_diff % 7
1904+
if days_forward <= 3:
1905+
# The upcoming self.weekday is closer than the previous one
1906+
return target_date + timedelta(days_forward)
1907+
else:
1908+
# The previous self.weekday is closer than the upcoming one
1909+
return target_date + timedelta(days_forward - 7)
19261910

19271911
@property
19281912
def rule_code(self):

0 commit comments

Comments
 (0)