Skip to content

Commit 8486c73

Browse files
jbrockmendeljreback
authored andcommitted
move shift_months to liboffsets, unify implementations of Q/M/Y offsets, (pandas-dev#18375)
1 parent 7aa2737 commit 8486c73

File tree

5 files changed

+187
-321
lines changed

5 files changed

+187
-321
lines changed

pandas/_libs/tslib.pyx

-105
Original file line numberDiff line numberDiff line change
@@ -1734,10 +1734,6 @@ def monthrange(int64_t year, int64_t month):
17341734
return (dayofweek(year, month, 1), days)
17351735

17361736

1737-
cdef inline int days_in_month(pandas_datetimestruct dts) nogil:
1738-
return days_per_month_table[is_leapyear(dts.year)][dts.month - 1]
1739-
1740-
17411737
cpdef normalize_date(object dt):
17421738
"""
17431739
Normalize datetime.datetime value to midnight. Returns datetime.date as a
@@ -1756,104 +1752,3 @@ cpdef normalize_date(object dt):
17561752
return datetime(dt.year, dt.month, dt.day)
17571753
else:
17581754
raise TypeError('Unrecognized type: %s' % type(dt))
1759-
1760-
1761-
cdef inline int _year_add_months(pandas_datetimestruct dts, int months) nogil:
1762-
"""new year number after shifting pandas_datetimestruct number of months"""
1763-
return dts.year + (dts.month + months - 1) / 12
1764-
1765-
1766-
cdef inline int _month_add_months(pandas_datetimestruct dts, int months) nogil:
1767-
"""
1768-
New month number after shifting pandas_datetimestruct
1769-
number of months.
1770-
"""
1771-
cdef int new_month = (dts.month + months) % 12
1772-
return 12 if new_month == 0 else new_month
1773-
1774-
1775-
@cython.wraparound(False)
1776-
@cython.boundscheck(False)
1777-
def shift_months(int64_t[:] dtindex, int months, object day=None):
1778-
"""
1779-
Given an int64-based datetime index, shift all elements
1780-
specified number of months using DateOffset semantics
1781-
1782-
day: {None, 'start', 'end'}
1783-
* None: day of month
1784-
* 'start' 1st day of month
1785-
* 'end' last day of month
1786-
"""
1787-
cdef:
1788-
Py_ssize_t i
1789-
pandas_datetimestruct dts
1790-
int count = len(dtindex)
1791-
int months_to_roll
1792-
bint roll_check
1793-
int64_t[:] out = np.empty(count, dtype='int64')
1794-
1795-
if day is None:
1796-
with nogil:
1797-
for i in range(count):
1798-
if dtindex[i] == NPY_NAT:
1799-
out[i] = NPY_NAT
1800-
continue
1801-
1802-
dt64_to_dtstruct(dtindex[i], &dts)
1803-
dts.year = _year_add_months(dts, months)
1804-
dts.month = _month_add_months(dts, months)
1805-
1806-
dts.day = min(dts.day, days_in_month(dts))
1807-
out[i] = dtstruct_to_dt64(&dts)
1808-
elif day == 'start':
1809-
roll_check = False
1810-
if months <= 0:
1811-
months += 1
1812-
roll_check = True
1813-
with nogil:
1814-
for i in range(count):
1815-
if dtindex[i] == NPY_NAT:
1816-
out[i] = NPY_NAT
1817-
continue
1818-
1819-
dt64_to_dtstruct(dtindex[i], &dts)
1820-
months_to_roll = months
1821-
1822-
# offset semantics - if on the anchor point and going backwards
1823-
# shift to next
1824-
if roll_check and dts.day == 1:
1825-
months_to_roll -= 1
1826-
1827-
dts.year = _year_add_months(dts, months_to_roll)
1828-
dts.month = _month_add_months(dts, months_to_roll)
1829-
dts.day = 1
1830-
1831-
out[i] = dtstruct_to_dt64(&dts)
1832-
elif day == 'end':
1833-
roll_check = False
1834-
if months > 0:
1835-
months -= 1
1836-
roll_check = True
1837-
with nogil:
1838-
for i in range(count):
1839-
if dtindex[i] == NPY_NAT:
1840-
out[i] = NPY_NAT
1841-
continue
1842-
1843-
dt64_to_dtstruct(dtindex[i], &dts)
1844-
months_to_roll = months
1845-
1846-
# similar semantics - when adding shift forward by one
1847-
# month if already at an end of month
1848-
if roll_check and dts.day == days_in_month(dts):
1849-
months_to_roll += 1
1850-
1851-
dts.year = _year_add_months(dts, months_to_roll)
1852-
dts.month = _month_add_months(dts, months_to_roll)
1853-
1854-
dts.day = days_in_month(dts)
1855-
out[i] = dtstruct_to_dt64(&dts)
1856-
else:
1857-
raise ValueError("day must be None, 'start' or 'end'")
1858-
1859-
return np.asarray(out)

pandas/_libs/tslibs/offsets.pyx

+115-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# cython: profile=False
33

44
cimport cython
5+
from cython cimport Py_ssize_t
56

67
import time
78
from cpython.datetime cimport datetime, timedelta, time as dt_time
@@ -10,6 +11,7 @@ from dateutil.relativedelta import relativedelta
1011

1112
import numpy as np
1213
cimport numpy as np
14+
from numpy cimport int64_t
1315
np.import_array()
1416

1517

@@ -19,6 +21,10 @@ from pandas._libs.tslib import monthrange
1921

2022
from conversion cimport tz_convert_single, pydt_to_i8
2123
from frequencies cimport get_freq_code
24+
from nattype cimport NPY_NAT
25+
from np_datetime cimport (pandas_datetimestruct,
26+
dtstruct_to_dt64, dt64_to_dtstruct,
27+
is_leapyear, days_per_month_table)
2228

2329
# ---------------------------------------------------------------------
2430
# Constants
@@ -419,13 +425,121 @@ class BaseOffset(_BaseOffset):
419425
# ----------------------------------------------------------------------
420426
# RelativeDelta Arithmetic
421427

428+
@cython.wraparound(False)
429+
@cython.boundscheck(False)
430+
cdef inline int get_days_in_month(int year, int month) nogil:
431+
return days_per_month_table[is_leapyear(year)][month - 1]
432+
433+
434+
cdef inline int year_add_months(pandas_datetimestruct dts, int months) nogil:
435+
"""new year number after shifting pandas_datetimestruct number of months"""
436+
return dts.year + (dts.month + months - 1) / 12
437+
438+
439+
cdef inline int month_add_months(pandas_datetimestruct dts, int months) nogil:
440+
"""
441+
New month number after shifting pandas_datetimestruct
442+
number of months.
443+
"""
444+
cdef int new_month = (dts.month + months) % 12
445+
return 12 if new_month == 0 else new_month
446+
447+
448+
@cython.wraparound(False)
449+
@cython.boundscheck(False)
450+
def shift_months(int64_t[:] dtindex, int months, object day=None):
451+
"""
452+
Given an int64-based datetime index, shift all elements
453+
specified number of months using DateOffset semantics
454+
455+
day: {None, 'start', 'end'}
456+
* None: day of month
457+
* 'start' 1st day of month
458+
* 'end' last day of month
459+
"""
460+
cdef:
461+
Py_ssize_t i
462+
pandas_datetimestruct dts
463+
int count = len(dtindex)
464+
int months_to_roll
465+
bint roll_check
466+
int64_t[:] out = np.empty(count, dtype='int64')
467+
468+
if day is None:
469+
with nogil:
470+
for i in range(count):
471+
if dtindex[i] == NPY_NAT:
472+
out[i] = NPY_NAT
473+
continue
474+
475+
dt64_to_dtstruct(dtindex[i], &dts)
476+
dts.year = year_add_months(dts, months)
477+
dts.month = month_add_months(dts, months)
478+
479+
dts.day = min(dts.day, get_days_in_month(dts.year, dts.month))
480+
out[i] = dtstruct_to_dt64(&dts)
481+
elif day == 'start':
482+
roll_check = False
483+
if months <= 0:
484+
months += 1
485+
roll_check = True
486+
with nogil:
487+
for i in range(count):
488+
if dtindex[i] == NPY_NAT:
489+
out[i] = NPY_NAT
490+
continue
491+
492+
dt64_to_dtstruct(dtindex[i], &dts)
493+
months_to_roll = months
494+
495+
# offset semantics - if on the anchor point and going backwards
496+
# shift to next
497+
if roll_check and dts.day == 1:
498+
months_to_roll -= 1
499+
500+
dts.year = year_add_months(dts, months_to_roll)
501+
dts.month = month_add_months(dts, months_to_roll)
502+
dts.day = 1
503+
504+
out[i] = dtstruct_to_dt64(&dts)
505+
elif day == 'end':
506+
roll_check = False
507+
if months > 0:
508+
months -= 1
509+
roll_check = True
510+
with nogil:
511+
for i in range(count):
512+
if dtindex[i] == NPY_NAT:
513+
out[i] = NPY_NAT
514+
continue
515+
516+
dt64_to_dtstruct(dtindex[i], &dts)
517+
months_to_roll = months
518+
519+
# similar semantics - when adding shift forward by one
520+
# month if already at an end of month
521+
if roll_check and dts.day == get_days_in_month(dts.year,
522+
dts.month):
523+
months_to_roll += 1
524+
525+
dts.year = year_add_months(dts, months_to_roll)
526+
dts.month = month_add_months(dts, months_to_roll)
527+
528+
dts.day = get_days_in_month(dts.year, dts.month)
529+
out[i] = dtstruct_to_dt64(&dts)
530+
else:
531+
raise ValueError("day must be None, 'start' or 'end'")
532+
533+
return np.asarray(out)
534+
535+
422536
cpdef datetime shift_month(datetime stamp, int months, object day_opt=None):
423537
"""
424538
Given a datetime (or Timestamp) `stamp`, an integer `months` and an
425539
option `day_opt`, return a new datetimelike that many months later,
426540
with day determined by `day_opt` using relativedelta semantics.
427541
428-
Scalar analogue of tslib.shift_months
542+
Scalar analogue of shift_months
429543
430544
Parameters
431545
----------

pandas/tests/indexes/datetimes/test_ops.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from itertools import product
99
import pandas as pd
1010
import pandas._libs.tslib as tslib
11+
from pandas._libs.tslibs.offsets import shift_months
1112
import pandas.util.testing as tm
1213
from pandas import (DatetimeIndex, PeriodIndex, Series, Timestamp,
1314
date_range, _np_version_under1p10, Index,
@@ -668,8 +669,7 @@ def test_shift_months(years, months):
668669
Timestamp('2000-01-01'),
669670
Timestamp('2000-02-29'),
670671
Timestamp('2000-12-31')])
671-
actual = DatetimeIndex(tslib.shift_months(s.asi8, years * 12 +
672-
months))
672+
actual = DatetimeIndex(shift_months(s.asi8, years * 12 + months))
673673
expected = DatetimeIndex([x + pd.offsets.DateOffset(
674674
years=years, months=months) for x in s])
675675
tm.assert_index_equal(actual, expected)

0 commit comments

Comments
 (0)