Skip to content

Commit 1e83d87

Browse files
jbrockmendeljreback
authored andcommitted
simplify algebra in Year offset apply methods (#18280)
1 parent fe1bfd7 commit 1e83d87

File tree

3 files changed

+175
-73
lines changed

3 files changed

+175
-73
lines changed

pandas/_libs/tslibs/offsets.pyx

+102
Original file line numberDiff line numberDiff line change
@@ -428,3 +428,105 @@ cpdef datetime shift_month(datetime stamp, int months, object day_opt=None):
428428
else:
429429
raise ValueError(day_opt)
430430
return stamp.replace(year=year, month=month, day=day)
431+
432+
433+
cdef int get_day_of_month(datetime other, day_opt) except? -1:
434+
"""
435+
Find the day in `other`'s month that satisfies a DateOffset's onOffset
436+
policy, as described by the `day_opt` argument.
437+
438+
Parameters
439+
----------
440+
other : datetime or Timestamp
441+
day_opt : 'start', 'end'
442+
'start': returns 1
443+
'end': returns last day of the month
444+
445+
Returns
446+
-------
447+
day_of_month : int
448+
449+
Examples
450+
-------
451+
>>> other = datetime(2017, 11, 14)
452+
>>> get_day_of_month(other, 'start')
453+
1
454+
>>> get_day_of_month(other, 'end')
455+
30
456+
457+
"""
458+
if day_opt == 'start':
459+
return 1
460+
elif day_opt == 'end':
461+
return monthrange(other.year, other.month)[1]
462+
else:
463+
raise ValueError(day_opt)
464+
465+
466+
cpdef int roll_yearday(other, n, month, day_opt='start') except? -1:
467+
"""
468+
Possibly increment or decrement the number of periods to shift
469+
based on rollforward/rollbackward conventions.
470+
471+
Parameters
472+
----------
473+
other : datetime or Timestamp
474+
n : number of periods to increment, before adjusting for rolling
475+
day_opt : 'start', 'end'
476+
'start': returns 1
477+
'end': returns last day of the month
478+
479+
Returns
480+
-------
481+
n : int number of periods to increment
482+
483+
Notes
484+
-----
485+
* Mirrors `roll_check` in tslib.shift_months
486+
487+
Examples
488+
-------
489+
>>> month = 3
490+
>>> day_opt = 'start' # `other` will be compared to March 1
491+
>>> other = datetime(2017, 2, 10) # before March 1
492+
>>> roll_yearday(other, 2, month, day_opt)
493+
1
494+
>>> roll_yearday(other, -7, month, day_opt)
495+
-7
496+
>>>
497+
>>> other = Timestamp('2014-03-15', tz='US/Eastern') # after March 1
498+
>>> roll_yearday(other, 2, month, day_opt)
499+
2
500+
>>> roll_yearday(other, -7, month, day_opt)
501+
-6
502+
503+
>>> month = 6
504+
>>> day_opt = 'end' # `other` will be compared to June 30
505+
>>> other = datetime(1999, 6, 29) # before June 30
506+
>>> roll_yearday(other, 5, month, day_opt)
507+
4
508+
>>> roll_yearday(other, -7, month, day_opt)
509+
-7
510+
>>>
511+
>>> other = Timestamp(2072, 8, 24, 6, 17, 18) # after June 30
512+
>>> roll_yearday(other, 5, month, day_opt)
513+
5
514+
>>> roll_yearday(other, -7, month, day_opt)
515+
-6
516+
517+
"""
518+
# Note: The other.day < ... condition will never hold when day_opt=='start'
519+
# and the other.day > ... condition will never hold when day_opt=='end'.
520+
# At some point these extra checks may need to be optimized away.
521+
# But that point isn't today.
522+
if n > 0:
523+
if other.month < month or (other.month == month and
524+
other.day < get_day_of_month(other,
525+
day_opt)):
526+
n -= 1
527+
elif n <= 0:
528+
if other.month > month or (other.month == month and
529+
other.day > get_day_of_month(other,
530+
day_opt)):
531+
n += 1
532+
return n
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Tests for helper functions in the cython tslibs.offsets
4+
"""
5+
from datetime import datetime
6+
7+
import pytest
8+
9+
import pandas as pd
10+
11+
import pandas._libs.tslibs.offsets as liboffsets
12+
13+
14+
def test_shift_month():
15+
dt = datetime(2017, 11, 15)
16+
17+
assert liboffsets.shift_month(dt, 0, day_opt=None) == dt
18+
assert liboffsets.shift_month(dt, 0, day_opt=15) == dt
19+
20+
assert liboffsets.shift_month(dt, 1,
21+
day_opt='start') == datetime(2017, 12, 1)
22+
23+
assert liboffsets.shift_month(dt, -145,
24+
day_opt='end') == datetime(2005, 10, 31)
25+
26+
with pytest.raises(ValueError):
27+
liboffsets.shift_month(dt, 3, day_opt='this should raise')
28+
29+
30+
def test_get_day_of_month():
31+
# get_day_of_month is not directly exposed; we test it via roll_yearday
32+
dt = datetime(2017, 11, 15)
33+
34+
with pytest.raises(ValueError):
35+
# To hit the raising case we need month == dt.month and n > 0
36+
liboffsets.roll_yearday(dt, n=3, month=11, day_opt='foo')
37+
38+
39+
def test_roll_yearday():
40+
# Copied from doctest examples
41+
month = 3
42+
day_opt = 'start' # `other` will be compared to March 1
43+
other = datetime(2017, 2, 10) # before March 1
44+
assert liboffsets.roll_yearday(other, 2, month, day_opt) == 1
45+
assert liboffsets.roll_yearday(other, -7, month, day_opt) == -7
46+
assert liboffsets.roll_yearday(other, 0, month, day_opt) == 0
47+
48+
other = pd.Timestamp('2014-03-15', tz='US/Eastern') # after March 1
49+
assert liboffsets.roll_yearday(other, 2, month, day_opt) == 2
50+
assert liboffsets.roll_yearday(other, -7, month, day_opt) == -6
51+
assert liboffsets.roll_yearday(other, 0, month, day_opt) == 1
52+
53+
month = 6
54+
day_opt = 'end' # `other` will be compared to June 30
55+
other = datetime(1999, 6, 29) # before June 30
56+
assert liboffsets.roll_yearday(other, 5, month, day_opt) == 4
57+
assert liboffsets.roll_yearday(other, -7, month, day_opt) == -7
58+
assert liboffsets.roll_yearday(other, 0, month, day_opt) == 0
59+
60+
other = pd.Timestamp(2072, 8, 24, 6, 17, 18) # after June 30
61+
assert liboffsets.roll_yearday(other, 5, month, day_opt) == 5
62+
assert liboffsets.roll_yearday(other, -7, month, day_opt) == -6
63+
assert liboffsets.roll_yearday(other, 0, month, day_opt) == 1

pandas/tseries/offsets.py

+10-73
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
_int_to_weekday, _weekday_to_int,
2323
_determine_offset,
2424
apply_index_wraps,
25+
roll_yearday,
2526
shift_month,
2627
BeginMixin, EndMixin,
2728
BaseOffset)
@@ -1905,49 +1906,12 @@ class YearEnd(EndMixin, YearOffset):
19051906

19061907
@apply_wraps
19071908
def apply(self, other):
1908-
def _increment(date):
1909-
if date.month == self.month:
1910-
_, days_in_month = tslib.monthrange(date.year, self.month)
1911-
if date.day != days_in_month:
1912-
year = date.year
1913-
else:
1914-
year = date.year + 1
1915-
elif date.month < self.month:
1916-
year = date.year
1917-
else:
1918-
year = date.year + 1
1919-
_, days_in_month = tslib.monthrange(year, self.month)
1920-
return datetime(year, self.month, days_in_month,
1921-
date.hour, date.minute, date.second,
1922-
date.microsecond)
1923-
1924-
def _decrement(date):
1925-
year = date.year if date.month > self.month else date.year - 1
1926-
_, days_in_month = tslib.monthrange(year, self.month)
1927-
return datetime(year, self.month, days_in_month,
1928-
date.hour, date.minute, date.second,
1929-
date.microsecond)
1930-
1931-
def _rollf(date):
1932-
if date.month != self.month or\
1933-
date.day < tslib.monthrange(date.year, date.month)[1]:
1934-
date = _increment(date)
1935-
return date
1936-
1937-
n = self.n
1938-
result = other
1939-
if n > 0:
1940-
while n > 0:
1941-
result = _increment(result)
1942-
n -= 1
1943-
elif n < 0:
1944-
while n < 0:
1945-
result = _decrement(result)
1946-
n += 1
1947-
else:
1948-
# n == 0, roll forward
1949-
result = _rollf(result)
1950-
return result
1909+
n = roll_yearday(other, self.n, self.month, 'end')
1910+
year = other.year + n
1911+
days_in_month = tslib.monthrange(year, self.month)[1]
1912+
return datetime(year, self.month, days_in_month,
1913+
other.hour, other.minute, other.second,
1914+
other.microsecond)
19511915

19521916
@apply_index_wraps
19531917
def apply_index(self, i):
@@ -1968,36 +1932,9 @@ class YearBegin(BeginMixin, YearOffset):
19681932

19691933
@apply_wraps
19701934
def apply(self, other):
1971-
def _increment(date, n):
1972-
year = date.year + n - 1
1973-
if date.month >= self.month:
1974-
year += 1
1975-
return datetime(year, self.month, 1, date.hour, date.minute,
1976-
date.second, date.microsecond)
1977-
1978-
def _decrement(date, n):
1979-
year = date.year + n + 1
1980-
if date.month < self.month or (date.month == self.month and
1981-
date.day == 1):
1982-
year -= 1
1983-
return datetime(year, self.month, 1, date.hour, date.minute,
1984-
date.second, date.microsecond)
1985-
1986-
def _rollf(date):
1987-
if (date.month != self.month) or date.day > 1:
1988-
date = _increment(date, 1)
1989-
return date
1990-
1991-
n = self.n
1992-
result = other
1993-
if n > 0:
1994-
result = _increment(result, n)
1995-
elif n < 0:
1996-
result = _decrement(result, n)
1997-
else:
1998-
# n == 0, roll forward
1999-
result = _rollf(result)
2000-
return result
1935+
n = roll_yearday(other, self.n, self.month, 'start')
1936+
year = other.year + n
1937+
return other.replace(year=year, month=self.month, day=1)
20011938

20021939
@apply_index_wraps
20031940
def apply_index(self, i):

0 commit comments

Comments
 (0)