Welcome, guest | Sign In | My Account | Store | Cart

Adding or subtracting a month to a Python datetime.date or datetime.datetime is a little bit of a pain. Here is the code I use for that. These functions return the same datetime type as given. They preserve time of day data (if that is at all important to you).

Python, 39 lines
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 def add_one_month(t): """Return a `datetime.date` or `datetime.datetime` (as given) that is one month earlier. Note that the resultant day of the month might change if the following month has fewer days: >>> add_one_month(datetime.date(2010, 1, 31)) datetime.date(2010, 2, 28) """ import datetime one_day = datetime.timedelta(days=1) one_month_later = t + one_day while one_month_later.month == t.month: # advance to start of next month one_month_later += one_day target_month = one_month_later.month while one_month_later.day < t.day: # advance to appropriate day one_month_later += one_day if one_month_later.month != target_month: # gone too far one_month_later -= one_day break return one_month_later def subtract_one_month(t): """Return a `datetime.date` or `datetime.datetime` (as given) that is one month later. Note that the resultant day of the month might change if the following month has fewer days: >>> subtract_one_month(datetime.date(2010, 3, 31)) datetime.date(2010, 2, 28) """ import datetime one_day = datetime.timedelta(days=1) one_month_earlier = t - one_day while one_month_earlier.month == t.month or one_month_earlier.day > t.day: one_month_earlier -= one_day return one_month_earlier Andreas Balogh 11 years, 6 months ago

There is a much simpler way. For finding the previous month you go to the first, then subtract one day:

from datetime import timedelta

def subtract_one_month(dt0):
dt1 = dt0.replace(days=1)
dt2 = dt1 - timedelta(days=1)
dt3 = dt2.replace(days=1)
return dt3

For finding the next month's first you advance to the next month. By adding 32 days from the first of a month you will always reach the next month.

dt1 = dt0.replace(days=1)
dt2 = dt1 + timedelta(days=32)
dt3 = dt2.replace(days=1)
return dt3 Trent Mick 11 years, 6 months ago

@andreas: Yup, thanks. For some of my use cases, preserving the day of month was important. jort.bloem 9 years, 6 months ago

What about this as an alternative; the range of months is limited by date or datetime, both are supported, and all data (day, and hour/minute/second if it's a datetime) are maintained. The code is also much shorter, and negative numbers are supported for subtracting months.

Note: months may be positive, or negative, but must be an integer.

def addmonths(date,months): targetmonth=months+date.month try: date.replace(year=date.year+int(targetmonth/12),month=(targetmonth%12)) except: # There is an exception if the day of the month we're in does not exist in the target month # Go to the FIRST of the month AFTER, then go back one day. date.replace(year=date.year+int((targetmonth+1)/12),month=((targetmonth+1)%12),day=1) date+=datetime.timedelta(days=-1) jort.bloem 9 years, 6 months ago

Lets try that again:

# Note: months may be positive, or negative, but must be an integer.
targetmonth=months+date.month
try:
date.replace(year=date.year+int(targetmonth/12),month=(targetmonth%12))
except:
# There is an exception if the day of the month we're in does not exist in the target month
# Go to the FIRST of the month AFTER, then go back one day.
date.replace(year=date.year+int((targetmonth+1)/12),month=((targetmonth+1)%12),day=1)
date+=datetime.timedelta(days=-1) Richard Fairhurst 9 years, 5 months ago

I think the code of jort.bloom is on the right track, but I believe errors will also always be generated when the target month is a multiple of 12, which returns a modulo remainder of 0. An error in the month will mess up the result. For example, addmonths(12/5/2011, 0) i.e., December 5, 2011 minus no months, will return 12/31/2011, not 12/5/2011, because an error will occur when month=(targetmonth%12) tries to set month=0, not month=12.

Also, when the input date is the last day of the month, often I favor using the end of the month of the offset date. So, while 7/31/2012 - 1 month = 6/30/2012 is good, 6/30/2012 - 1 month = 5/30/2012 is bad (I want 5/31/2012). A variable indicating that I want to favor the end of the month should be included so that I can specify if I want an end of the month date to offset to the closest matching day number in a month with more days (i.e, 30 in a 30 day month = 30 in a 31 day month) or to the closest end of the month day in a month with more days (30 in a 30 day month = 31 in a 31 day month).

Here is the way I would revise the code to deal with these issues:

# Note: months may be positive, or negative, but must be an integer.
# If favorEoM (favor End of Month) is true and input date is the last day of the month then
# return an offset date that also falls on the last day of the month.
try:
targetdate = date
targetmonths = months+targetdate.month
if targetmonths%12 = 0:
# Month must be between 1 and 12 so a modulo remainder of 0 = 12
targetmonth = 12
else:
targetmonth = targetmonths%12
if favorEoM == True:
# Favor matching an End of Month date to an End of Month offset date.
testdate = date+datetime.timedelta(days=1)
if testdate.day == 1:
# input date was a last day of month and end of month is favored, so
# go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
else:
# Do not favor matching an End of Month date to the offset End of Month.
targetdate.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
return targetdate
except:
# There is an exception if the day of the month we're in does not exist in the target month
# Go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
return targetdate Richard Fairhurst 9 years, 5 months ago

There was a missing else block in the code I posted. Here it is corrected.

# Note: months may be positive, or negative, but must be an integer.
# If favorEoM (favor End of Month) is true and input date is the last day of the month then
# return an offset date that also falls on the last day of the month.
try:
targetdate = date
targetmonths = months+targetdate.month
if targetmonths%12 = 0:
# Month must be between 1 and 12 so a modulo remainder of 0 = 12
targetmonth = 12
else:
targetmonth = targetmonths%12
if favorEoM == True:
# Favor matching an End of Month date to an End of Month offset date.
testdate = date+datetime.timedelta(days=1)
if testdate.day == 1:
# input date was a last day of month and end of month is favored, so
# go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
else:
targetdate.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
else:
# Do not favor matching an End of Month date to the offset End of Month.
targetdate.replace(year=dtargetdate.year+int(targetmonths/12),month=(targetmonth))
return targetdate
except:
# There is an exception if the day of the month we're in does not exist in the target month
# Go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
return targetdate Richard Fairhurst 9 years, 5 months ago

Oops. I caught a variable name spelling error. I wish I could edit my previous comments, but since I can't, here is the corrected code again:

# Note: months may be positive, or negative, but must be an integer.
# If favorEoM (favor End of Month) is true and input date is the last day of the month then
# return an offset date that also falls on the last day of the month.
try:
targetdate = date
targetmonths = months+targetdate.month
if targetmonths%12 = 0:
# Month must be between 1 and 12 so a modulo remainder of 0 = 12
targetmonth = 12
else:
targetmonth = targetmonths%12
if favorEoM == True:
# Favor matching an End of Month date to an End of Month offset date.
testdate = date+datetime.timedelta(days=1)
if testdate.day == 1:
# input date was a last day of month and end of month is favored, so
# go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
else:
targetdate.replace(year=targetdate.year+int(targetmonths/12),month=(targetmonth))
else:
# Do not favor matching an End of Month date to the offset End of Month.
targetdate.replace(year=targetdate.year+int(targetmonths/12),month=(targetmonth))
return targetdate
except:
# There is an exception if the day of the month we're in does not exist in the target month
# Go to the FIRST of the month AFTER, then go back one day.
targetdate.replace(year=targetdate.year+int((targetmonths+1)/12),month=(targetmonth%12+1),day=1)
targetdate+=datetime.timedelta(days=-1)
return targetdate Garel Alex 7 years, 3 months ago

My own:

"""add one month to date, maybe falling to last day of month

:param datetime.datetime date: the date

::
datetime.datetime(2014, 2, 28, 0, 0)
datetime.datetime(2015, 1, 30, 0, 0)
"""
# number of days this month
month_days = calendar.monthrange(date.year, date.month)
candidate = date + timedelta(days=month_days)
# but maybe we are a month too far
if candidate.day != date.day:
# go to last day of next month,
# by getting one day before begin of candidate month
return candidate.replace(day=1) - timedelta(days=1)
else:
return candidate Created by Trent Mick on Fri, 25 Jun 2010 (MIT)