7

I want to create a list (or array or whathever) of a given number of dates at monthly intervals.

Basically what I want is this

>>>some_function(start_date=date(2005, 5, 14), periods=4, freq='M')
['2005-05-14', '2005-06-14', '2005-07-14', '2005-08-14']

and if the day of the startmonth is close to end of the month I want this

>>>some_function(start_date=date(2007, 12, 31), periods=4, freq='M')
['2007-12-31', '2008-01-31', '2008-02-29', '2008-03-31']

I am aware of the pandas date_range function, however it produces this

pd.date_range(date(2005, 5, 14), periods=4, freq='M')
Out[1]: DatetimeIndex(['2005-05-31', '2005-06-30', '2005-07-31', '2005-08-31'],
          dtype='datetime64[ns]', freq='M')

i.e. it sets the month end as the day. Which is not what I want.

Obviously, this could be produced iterating over the number of periods, but this creates a hassle when the day of the startmonth is close to the last day of the month.

Does anybody know of a function producing this or is the method outlined above the only way?

mortysporty
  • 2,749
  • 6
  • 28
  • 51

3 Answers3

10

I think the behavior you're after is, you want a date range where all the dates are on the same day of the month as your start date, except use the last day of the month for months which have fewer days in the month.

You can do that by using pandas.DateOffset(months=1, day=day_of_month) as the freq argument, where day_of_month is the day of month you want each date to be on. This will automatically use the last day of the month for months whose last day is less than day_of_month.

In [68]: pandas.date_range('2005-05-14', periods=4, freq=pandas.DateOffset(months=1, day=14))
Out[68]: DatetimeIndex(['2005-05-14', '2005-06-14', '2005-07-14', '2005-08-14'], dtype='datetime64[ns]', freq='<DateOffset: day=14, months=1>')

In [69]: pandas.date_range('2007-12-31', periods=4, freq=pandas.DateOffset(months=1, day=31))
Out[69]: DatetimeIndex(['2007-12-31', '2008-01-31', '2008-02-29', '2008-03-31'], dtype='datetime64[ns]', freq='<DateOffset: day=31, months=1>')
user3311658
  • 346
  • 3
  • 3
6

I crafted the following:

from datetime import datetime, date
from datetime import timedelta

def next_month_generator(d):
   while True:
       if d.month == 12:
           # on Dec, need to change year
           d = d.replace(year=d.year+1, month=1)
       else:
           try:
               # get next month, same date
               d = d.replace(month=d.month+1)
           except ValueError:
               # get next month, last months date
               d = date(year=d.year, month=d.month+2, day=1) - timedelta(days=1)
       yield d

start_date=date(2017, 1, 31)
nm = next_month_generator(start_date)
for _ in range(13):
   print(nm.next())

 >> 2017-02-28
 >> 2017-03-28
 >> 2017-04-28
 >> 2017-05-28
 >> 2017-06-28
 >> 2017-07-28
 >> 2017-08-28
 >> 2017-09-28
 >> 2017-10-28
 >> 2017-11-28
 >> 2017-12-28
 >> 2018-01-28
 >> 2018-02-28

If Python 3, use:

for _ in range(13):
   print(next(nm))
Jarad
  • 17,409
  • 19
  • 95
  • 154
serkef
  • 319
  • 2
  • 13
  • 1
    upvoted. generator expressions are ideal for, well, generatable collections. – jpp Feb 02 '18 at 15:22
  • Hi. Thanks. Another Nice compact solution. It seems there is no built-in function and no way around a custom function. +1 – mortysporty Feb 02 '18 at 15:26
  • Yeah, what I don't like is that after we reach the first leap year february, we just iterate months in 1-12 range. – serkef Feb 02 '18 at 15:30
  • Yeah... that not optimal. I guess the day should be stored somehow and not taken from the previous answer – mortysporty Feb 02 '18 at 16:11
3

This should work. add_months function is via @DaveWebb in How to increment datetime by custom months in python without using library.

import datetime
import calendar

start_date = '2018-02-02'

def add_months(sourcedate, months):
    month = sourcedate.month - 1 + months
    year = sourcedate.year + month // 12
    month = month % 12 + 1
    day = min(sourcedate.day, calendar.monthrange(year, month)[1])
    return datetime.date(year, month, day)

def range_of_months(sourcedate, months):
    return [add_months(sourcedate, m) for m in range(months+1)]

start = datetime.date.today()

range_of_months(start, 5)

# [datetime.date(2018, 2, 2),
#  datetime.date(2018, 3, 2),
#  datetime.date(2018, 4, 2),
#  datetime.date(2018, 5, 2),
#  datetime.date(2018, 6, 2),
#  datetime.date(2018, 7, 2)]
jpp
  • 159,742
  • 34
  • 281
  • 339