22# cython: profile=False
33
44cimport cython
5+ from cython cimport Py_ssize_t
56
67import time
78from cpython.datetime cimport datetime, timedelta, time as dt_time
@@ -10,6 +11,7 @@ from dateutil.relativedelta import relativedelta
1011
1112import numpy as np
1213cimport numpy as np
14+ from numpy cimport int64_t
1315np.import_array()
1416
1517
@@ -19,6 +21,10 @@ from pandas._libs.tslib import monthrange
1921
2022from conversion cimport tz_convert_single, pydt_to_i8
2123from 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+
422536cpdef 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 ----------
0 commit comments