How do you add "3 months" to a datetime.date object in python?
Question:
Python date calculations, where art thou?
I have a python app that needs to plot out dates every three months for several years. It’s important that the dates occur exactly 4 times a year, and that the dates occur on the same day each year as much as possible, and that the dates occur on the same day of the month as much as possible, and that the dates be as close to “3 months” apart as they can be (which is a moving target, especially on leap year). Unfortunately, datetime.timedelta
doesn’t support months!
Is there a “standard” way to do this calculation in python???
The SQL way?
If worst comes to worst, I will punt and have my app ask PostgreSQL, who does have nice built-in support for date calculations, for the answer like this:
# select ('2010-11-29'::date + interval '3 months')::date;
date
------------
2011-02-28
(1 row)
Answers:
import datetime
some_date = datetime.date.today()
three_months = datetime.timedelta(3*365/12)
print (some_date + three_months).isoformat()
# => '2012-06-01'
Then “normalize” every new year to the original date’s day (unless Feb 29)
If you’re looking for exact or “more precise” dates, you’re probably better off checking out dateutil.
Quick example:
>>> from dateutil.relativedelta import relativedelta
>>> import datetime
>>> TODAY = datetime.date.today()
>>> TODAY
datetime.date(2012, 3, 6)
Now add 3 months to TODAY
, observe that it matches the day exactly (Note that relativedelta(months=3)
and relativedelta(month=3)
have different behaviors. Make sure to use months
for these examples!).
>>> three_mon_rel = relativedelta(months=3)
>>> TODAY + three_mon_rel
datetime.date(2012, 6, 6)
And it stays consistent throughout the course of a year. Literally every three months, on the day (had to keep adding because for some reason multiplying a relativedelta
and adding it to a datetime.date
object throws a TypeError
):
>>> TODAY + three_mon_rel + three_mon_rel
datetime.date(2012, 9, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2012, 12, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2013, 3, 6)
Whereas the mVChr‘s suggested solution, while definitely “good enough”, drifts slightly over time:
>>> three_mon_timedelta = datetime.timedelta(days=3 * 365/12)
>>> TODAY + three_mon_timedelta
datetime.date(2012, 6, 5)
And over the course of a year, the day of month keeps sliding:
>>> TODAY + three_mon_timedelta * 2
datetime.date(2012, 9, 4)
>>> TODAY + three_mon_timedelta * 3
datetime.date(2012, 12, 4)
>>> TODAY + three_mon_timedelta * 4
datetime.date(2013, 3, 5)
Using Python standard libraries, i.e. without dateutil
or others, and solving the ‘February 31st’ problem:
import datetime
import calendar
def add_months(date, months):
months_count = date.month + months
# Calculate the year
year = date.year + int(months_count / 12)
# Calculate the month
month = (months_count % 12)
if month == 0:
month = 12
# Calculate the day
day = date.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = datetime.date(year, month, day)
return new_date
Testing:
>>>date = datetime.date(2018, 11, 30)
>>>print(date, add_months(date, 3))
(datetime.date(2018, 11, 30), datetime.date(2019, 2, 28))
>>>print(date, add_months(date, 14))
(datetime.date(2018, 12, 31), datetime.date(2020, 2, 29))
I don’t have enough reputation to comment. So, I am just going to write up a solution that fixes a bug in the solution that David Ragazzi posted.
The error occurs when you add enough months to get to a December date. The year is 1 too many.
For example, add_months(date.fromisoformat('2020-01-29'), 11)
returns 2021 instead of 2020. I fixed the issue by changing the line starting with year =
.
import datetime
import calendar
def add_months(dateInput, months):
months_count = dateInput.month + months
# Calculate the year
year = dateInput.year + int((months_count-1) / 12)
# Calculate the month
month = (months_count % 12)
if month == 0:
month = 12
# Calculate the day
day = dateInput.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = date(year, month, day)
return new_date
The answers from @david-ragazzi and @mikedugas77 work well with positive values for months
, but not if months <= -date.month
. Here’s a modification that should work even for negative month offsets:
import calendar
import datetime
def add_months(date, months):
months_count = date.year * 12 + date.month + months - 1
# Calculate the year
year = months_count // 12
# Calculate the month
month = months_count % 12 + 1
# Calculate the day
day = date.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = datetime.date(year, month, day)
return new_date
With some tests using today’s date:
date = datetime.date(2020, 7, 30)
assert add_months(date, -12) == datetime.date(2019, 7, 30)
assert add_months(date, -11) == datetime.date(2019, 8, 30)
assert add_months(date, -10) == datetime.date(2019, 9, 30)
assert add_months(date, -9) == datetime.date(2019, 10, 30)
assert add_months(date, -8) == datetime.date(2019, 11, 30)
assert add_months(date, -7) == datetime.date(2019, 12, 30)
assert add_months(date, -6) == datetime.date(2020, 1, 30)
assert add_months(date, -5) == datetime.date(2020, 2, 29)
assert add_months(date, -4) == datetime.date(2020, 3, 30)
assert add_months(date, -3) == datetime.date(2020, 4, 30)
assert add_months(date, -2) == datetime.date(2020, 5, 30)
assert add_months(date, -1) == datetime.date(2020, 6, 30)
assert add_months(date, 0) == datetime.date(2020, 7, 30)
assert add_months(date, 1) == datetime.date(2020, 8, 30)
assert add_months(date, 2) == datetime.date(2020, 9, 30)
assert add_months(date, 3) == datetime.date(2020, 10, 30)
assert add_months(date, 4) == datetime.date(2020, 11, 30)
assert add_months(date, 5) == datetime.date(2020, 12, 30)
assert add_months(date, 6) == datetime.date(2021, 1, 30)
assert add_months(date, 7) == datetime.date(2021, 2, 28)
assert add_months(date, 8) == datetime.date(2021, 3, 30)
assert add_months(date, 9) == datetime.date(2021, 4, 30)
assert add_months(date, 10) == datetime.date(2021, 5, 30)
assert add_months(date, 11) == datetime.date(2021, 6, 30)
assert add_months(date, 12) == datetime.date(2021, 7, 30)
I wrote this function that may be able to help you:
import datetime
import calendar
def add_months(date, months):
# Determine the month and year of the new date
month, year = (date + relativedelta(months=months)).month, (date + relativedelta(months=months)).year
# Determine the day of the new date
# If the day of the current date is at the end of the month,
# the day of the new date should also be at the end of the month
if(date.day == calendar.monthrange(date.year, date.month)[1]):
day = calendar.monthrange(year, month)[1]
else:
day = date.day
new_date = datetime.datetime(year, month, day)
return new_date
It supports adding negative months as well (i.e. subtracting months).
Here is some sample usage that illustrates how to obtain 2021 and 2022 dates as per your specs:
import datetime
a = datetime.datetime(2020, 1, 1)
# Initialse a list to hold the dates
dates = [0]*8
# Obtain the dates
for i in range(0, len(dates)):
dates[i] = add_months(a, 3*i)
dates
Python date calculations, where art thou?
I have a python app that needs to plot out dates every three months for several years. It’s important that the dates occur exactly 4 times a year, and that the dates occur on the same day each year as much as possible, and that the dates occur on the same day of the month as much as possible, and that the dates be as close to “3 months” apart as they can be (which is a moving target, especially on leap year). Unfortunately, datetime.timedelta
doesn’t support months!
Is there a “standard” way to do this calculation in python???
The SQL way?
If worst comes to worst, I will punt and have my app ask PostgreSQL, who does have nice built-in support for date calculations, for the answer like this:
# select ('2010-11-29'::date + interval '3 months')::date;
date
------------
2011-02-28
(1 row)
import datetime
some_date = datetime.date.today()
three_months = datetime.timedelta(3*365/12)
print (some_date + three_months).isoformat()
# => '2012-06-01'
Then “normalize” every new year to the original date’s day (unless Feb 29)
If you’re looking for exact or “more precise” dates, you’re probably better off checking out dateutil.
Quick example:
>>> from dateutil.relativedelta import relativedelta
>>> import datetime
>>> TODAY = datetime.date.today()
>>> TODAY
datetime.date(2012, 3, 6)
Now add 3 months to TODAY
, observe that it matches the day exactly (Note that relativedelta(months=3)
and relativedelta(month=3)
have different behaviors. Make sure to use months
for these examples!).
>>> three_mon_rel = relativedelta(months=3)
>>> TODAY + three_mon_rel
datetime.date(2012, 6, 6)
And it stays consistent throughout the course of a year. Literally every three months, on the day (had to keep adding because for some reason multiplying a relativedelta
and adding it to a datetime.date
object throws a TypeError
):
>>> TODAY + three_mon_rel + three_mon_rel
datetime.date(2012, 9, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2012, 12, 6)
>>> TODAY + three_mon_rel + three_mon_rel + three_mon_rel + three_mon_rel
datetime.date(2013, 3, 6)
Whereas the mVChr‘s suggested solution, while definitely “good enough”, drifts slightly over time:
>>> three_mon_timedelta = datetime.timedelta(days=3 * 365/12)
>>> TODAY + three_mon_timedelta
datetime.date(2012, 6, 5)
And over the course of a year, the day of month keeps sliding:
>>> TODAY + three_mon_timedelta * 2
datetime.date(2012, 9, 4)
>>> TODAY + three_mon_timedelta * 3
datetime.date(2012, 12, 4)
>>> TODAY + three_mon_timedelta * 4
datetime.date(2013, 3, 5)
Using Python standard libraries, i.e. without dateutil
or others, and solving the ‘February 31st’ problem:
import datetime
import calendar
def add_months(date, months):
months_count = date.month + months
# Calculate the year
year = date.year + int(months_count / 12)
# Calculate the month
month = (months_count % 12)
if month == 0:
month = 12
# Calculate the day
day = date.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = datetime.date(year, month, day)
return new_date
Testing:
>>>date = datetime.date(2018, 11, 30)
>>>print(date, add_months(date, 3))
(datetime.date(2018, 11, 30), datetime.date(2019, 2, 28))
>>>print(date, add_months(date, 14))
(datetime.date(2018, 12, 31), datetime.date(2020, 2, 29))
I don’t have enough reputation to comment. So, I am just going to write up a solution that fixes a bug in the solution that David Ragazzi posted.
The error occurs when you add enough months to get to a December date. The year is 1 too many.
For example, add_months(date.fromisoformat('2020-01-29'), 11)
returns 2021 instead of 2020. I fixed the issue by changing the line starting with year =
.
import datetime
import calendar
def add_months(dateInput, months):
months_count = dateInput.month + months
# Calculate the year
year = dateInput.year + int((months_count-1) / 12)
# Calculate the month
month = (months_count % 12)
if month == 0:
month = 12
# Calculate the day
day = dateInput.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = date(year, month, day)
return new_date
The answers from @david-ragazzi and @mikedugas77 work well with positive values for months
, but not if months <= -date.month
. Here’s a modification that should work even for negative month offsets:
import calendar
import datetime
def add_months(date, months):
months_count = date.year * 12 + date.month + months - 1
# Calculate the year
year = months_count // 12
# Calculate the month
month = months_count % 12 + 1
# Calculate the day
day = date.day
last_day_of_month = calendar.monthrange(year, month)[1]
if day > last_day_of_month:
day = last_day_of_month
new_date = datetime.date(year, month, day)
return new_date
With some tests using today’s date:
date = datetime.date(2020, 7, 30)
assert add_months(date, -12) == datetime.date(2019, 7, 30)
assert add_months(date, -11) == datetime.date(2019, 8, 30)
assert add_months(date, -10) == datetime.date(2019, 9, 30)
assert add_months(date, -9) == datetime.date(2019, 10, 30)
assert add_months(date, -8) == datetime.date(2019, 11, 30)
assert add_months(date, -7) == datetime.date(2019, 12, 30)
assert add_months(date, -6) == datetime.date(2020, 1, 30)
assert add_months(date, -5) == datetime.date(2020, 2, 29)
assert add_months(date, -4) == datetime.date(2020, 3, 30)
assert add_months(date, -3) == datetime.date(2020, 4, 30)
assert add_months(date, -2) == datetime.date(2020, 5, 30)
assert add_months(date, -1) == datetime.date(2020, 6, 30)
assert add_months(date, 0) == datetime.date(2020, 7, 30)
assert add_months(date, 1) == datetime.date(2020, 8, 30)
assert add_months(date, 2) == datetime.date(2020, 9, 30)
assert add_months(date, 3) == datetime.date(2020, 10, 30)
assert add_months(date, 4) == datetime.date(2020, 11, 30)
assert add_months(date, 5) == datetime.date(2020, 12, 30)
assert add_months(date, 6) == datetime.date(2021, 1, 30)
assert add_months(date, 7) == datetime.date(2021, 2, 28)
assert add_months(date, 8) == datetime.date(2021, 3, 30)
assert add_months(date, 9) == datetime.date(2021, 4, 30)
assert add_months(date, 10) == datetime.date(2021, 5, 30)
assert add_months(date, 11) == datetime.date(2021, 6, 30)
assert add_months(date, 12) == datetime.date(2021, 7, 30)
I wrote this function that may be able to help you:
import datetime
import calendar
def add_months(date, months):
# Determine the month and year of the new date
month, year = (date + relativedelta(months=months)).month, (date + relativedelta(months=months)).year
# Determine the day of the new date
# If the day of the current date is at the end of the month,
# the day of the new date should also be at the end of the month
if(date.day == calendar.monthrange(date.year, date.month)[1]):
day = calendar.monthrange(year, month)[1]
else:
day = date.day
new_date = datetime.datetime(year, month, day)
return new_date
It supports adding negative months as well (i.e. subtracting months).
Here is some sample usage that illustrates how to obtain 2021 and 2022 dates as per your specs:
import datetime
a = datetime.datetime(2020, 1, 1)
# Initialse a list to hold the dates
dates = [0]*8
# Obtain the dates
for i in range(0, len(dates)):
dates[i] = add_months(a, 3*i)
dates