1

I am trying to come up with a way to create a list of dates n months back from given date dt. However, it seems to tricky based on what dt is. Below I am illustrating the dilemma through a few examples (esp. look at tricky case-3 below):

from datetime import datetime
from dateutil.relativedelta import relativedelta
# Simple case.
dt = datetime(2021, 2, 15)
dt - relativedelta(months=1)  # n=1 gives datetime.datetime(2021, 1, 15, 0, 0)
dt - relativedelta(months=2)  # n=2 gives datetime.datetime(2020, 12, 15, 0, 0)
# Simple case-2
dt = datetime(2021, 3, 31)
dt - relativedelta(months=1)  # n=1 gives datetime.datetime(2021, 2, 28, 0, 0)
dt - relativedelta(months=2)  # n=2 gives datetime.datetime(2021, 1, 31, 0, 0)
dt - relativedelta(months=3)  # n=3 gives datetime.datetime(2020, 12, 31, 0, 0)
dt - relativedelta(months=4)  # n=4 gives datetime.datetime(2020, 11, 30, 0, 0)
# Tricky case-3
dt = datetime(2021, 2, 28)
dt - relativedelta(months=1)  # n=1 gives datetime.datetime(2021, 1, 28, 0, 0) and not datetime.datetime(2021, 1, 31, 0, 0)
dt - relativedelta(months=2)  # n=2 gives datetime.datetime(2020, 12, 28, 0, 0) and not datetime.datetime(2020, 12, 31, 0, 0)
dt - relativedelta(months=3)  # n=3 gives datetime.datetime(2020, 11, 28, 0, 0) and not datetime.datetime(2020, 11, 30, 0, 0)
dt - relativedelta(months=4)  # n=4 gives datetime.datetime(2020, 10, 28, 0, 0) and not datetime.datetime(2020, 10, 31, 0, 0)
FObersteiner
  • 22,500
  • 8
  • 42
  • 72
Gerry
  • 606
  • 6
  • 16
  • relativedelta in this application not only fails for Feb 28th, but anytime a month has less than 31 days and `dt` represents the end of the month. – FObersteiner Aug 22 '21 at 14:15
  • Thanks @MrFuppes that is exactly the issue. I was thinking if there is a more natural way. – Gerry Aug 22 '21 at 18:24

1 Answers1

1

relativedelta seems to give unexpected results on the corner case of date is end of month while month has less than 31 days. Here's a work-around:

  • check if date is end of month
  • if not, simply use relativedelta
  • if so, use relativedelta but make sure the day is the last of the month by setting the day attribute explicitly
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta

# add_month adds n months to datetime object dt
def add_month(dt, n):
    # we can add a day without month changing - not end of month:
    if (dt + timedelta(1)).month == dt.month:
        return dt + relativedelta(months=n)
    # implicit else: end of month
    return (dt + relativedelta(months=n+1)).replace(day=1) - timedelta(1)

Examples:

d = datetime(2021, 3, 15)
print(add_month(d, -1).date(), d.date(), add_month(d, 1).date())
# 2021-02-15 2021-03-15 2021-04-15

d = datetime(2021, 3, 31)
print(add_month(d, -1).date(), d.date(), add_month(d, 1).date())
# 2021-02-28 2021-03-31 2021-04-30

d = datetime(2021,2,28)
print(add_month(d, -1).date(), d.date(), add_month(d, 1).date())
# 2021-01-31 2021-02-28 2021-03-31

d = datetime(2021,11,30)
print(add_month(d, -1).date(), d.date(), add_month(d, 1).date())
# 2021-10-31 2021-11-30 2021-12-31
FObersteiner
  • 22,500
  • 8
  • 42
  • 72
  • this misses 1/30/2020 – dataviews Apr 12 '23 at 23:12
  • @dataviews what do you mean by "it misses"? As I've commented on your (deleted) question, I think this approach *will* create duplicate dates and miss dates. However, here, it was not the question to avoid that. – FObersteiner Apr 13 '23 at 08:21