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?

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".