Dates are hard
It's February 29, so lets talk about it. It is such a fun edge case when dealing with dates.
Math with dates
Adding minutes, days or even weeks to a date is straight forward enough. In Python,
we can use timedelta
for this.
>>> # One week from today is:
>>> date(2016, 2, 29) + timedelta(days=7)
datetime.date(2016, 3, 7)
But timedelta
does not operate in months or years. We could just use the average
lengths in days, i.e. 365.2425 days per year, and 30.436875 days per month.
>>> # One average month from today
>>> date(2016, 2, 29) + timedelta(days=30.436875)
datetime.date(2016, 3, 30)
>>> # Six average months from today
>>> date(2016, 2, 29) + timedelta(days=6*30.436875)
datetime.date(2016, 8, 29)
What do you think? Close enough?
>>> # How about one month from January 1
>>> date(2016, 1, 1) + timedelta(days=30.436875)
datetime.date(2016, 1, 31)
If everybody is in agreement about using 30.44 days, this is fine. But how many strangers on the street would say "One month from January 1? Well, that is January 31."?
Adding a month - or a year - to a date, for most people is adding to the month value and leaving the other numbers in place. So one month from January 1 is February 1. Obviously.
So why don't we just do the same.
>>> jan1 = date(2016, 1, 1)
>>> # one month from jan 1
... jan1.replace(month=jan1.month + 1)
datetime.date(2016, 2, 1)
Perfect! But wait.. what about one month from January 30th?
>>> jan30 = date(2016, 1, 30)
>>> jan30.replace(month=jan30.month + 1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: day is out of range for month
Oh no that's no good.
What is January 30 plus 1 month?
- February 30 does not exist.
- February 29 is the last day of February this year, is that close enough?
- Is March 1 more appropriate substitute for February 30?
- Maybe we would prefer February 28. Maybe we are targeting the second last day of next month.
The dateutil.relativedelta
package provides one solution.
>>> from dateutil.relativedelta import relativedelta
>>> date(2016, 1, 30) + relativedelta(months=1)
datetime.date(2016, 2, 29)
>>> date(2017, 1, 30) + relativedelta(months=1)
datetime.date(2017, 2, 28)
>>> date(2016, 2, 29) + relativedelta(years=1)
datetime.date(2017, 2, 28)
>>> date(2016, 2, 29) + relativedelta(years=4)
datetime.date(2020, 2, 29)
As you can see, relativedelta
does handle the "day is out of range for month" issue above by rolling back to the last available day of the month. This is a sane default.
However, if there is an actual requirement for handling the edge cases differently, then you may need a custom solution.
Legally, for example in the United Kingdom, if a person is born on February 29 of a leap year, then on non leap years his birthday for determining legal age is on March 1, not February 28 (Wikipedia).
As stated in the title: "Dates are hard".