scls
scls

Reputation: 17607

Add one month to a given date (rounded day after) with Python

I'd like to add one month to a given date

import datetime
dt = datetime.datetime(year=2014, month=5, day=2)

so I should get

datetime.datetime(year=2014, month=6, day=2)

but with

dt = datetime.datetime(year=2015, month=1, day=31)

I should get

datetime.datetime(year=2015, month=3, day=1)

because there is no 2015-02-31 (and I want my result being round one day after)

Some months have 31 days, some other 30, some 29, some 28 !

so adding a datetime.timedelta is probably not a good manner of doing (because we don't know the number of days to add)

I noticed that Pandas have an interesting concept of DateOffset

http://pandas.pydata.org/pandas-docs/stable/timeseries.html#dateoffset-objects

but I didn't find a Month offset, just MonthBegin or MonthEnd

I see also this post How do I calculate the date six months from the current date using the datetime Python module?

so I tried dateutil.relativedelta but

from dateutil.relativedelta import relativedelta
datetime.datetime(year=2015, month=1, day=31)+relativedelta(months=1)

returns

datetime.datetime(2015, 2, 28, 0, 0)

so result was rounded one day before.

Is there a (clean) way to round day after ?

edit: I gave an example with one month to add but I also want to be able to add for example : 2 years and 6 months (using a relativedelta(years=2, months=6))

Upvotes: 11

Views: 9205

Answers (4)

aircraft
aircraft

Reputation: 26886

Use the bellow method:

def datetime_offset_by_months(datetime_origin, n=1):
    """
    datetime offset by months
    :param datetime_origin: the original datetime
    :param n: count of months
    :return: after offset datetime
    """
    # create a shortcut object for one day
    one_day = datetime.timedelta(days=1)

    # first use div and mod to determine year cycle
    q, r = divmod(datetime_origin.month + n, 12)

    # create a datetime_offset
    # to be the last day of the target month
    datetime_offset = datetime.datetime(
        datetime_origin.year + q, r + 1, 1) - one_day

    '''
       if input date is the last day of this month
       then the output date should also be the last
       day of the target month, although the day
       www.iplaypy.com
       may be different.
       for example:
       datetime_origin = 8.31
       datetime_offset = 9.30
    '''

    if datetime_origin.month != (datetime_origin + one_day).month:
        return datetime_offset

    '''
        if datetime_origin day is bigger than last day of
        target month, then, use datetime_offset
        for example:
        datetime_origin = 10.31
        datetime_offset = 11.30
    '''

    if datetime_origin.day >= datetime_offset.day:
        return datetime_offset

    '''
     then, here, we just replace datetime_offset's day
     with the same of datetime_origin, that's ok.
    '''

    return datetime_offset.replace(day= datetime_origin.day)

the usage of it:

import datetime
now_date = datetime.datetime.now()

off_set_datetime = datetime_offset_by_months(now_date, -2)
print(off_set_datetime)

Upvotes: 0

Ffisegydd
Ffisegydd

Reputation: 53678

You can use dateutil.relativedelta.relativedelta and manually check the datetime.day attribute, if the original day is greater than the new day, then add a day.

The function below accepts a datetime object and relativedelta object. Note that the code below only works for years and months, I don't believe it'll work if you use anything below that (days, hours, etc). You could easily modify this function to take years and months as arguments and then construct the relativedelta inside the function itself.

from datetime import datetime
from dateutil.relativedelta import relativedelta

def add_time(d, rd):
    day = relativedelta(days=+1)

    out = d + rd
    if d.day > out.day:
        out = out + day

    return out    

# Check that it "rolls over"
print(add_time(datetime(year=2015, month=1, day=29), relativedelta(years=+4, months=+1))) # 2019-03-01 00:00:00
print(add_time(datetime(year=2015, month=3, day=31), relativedelta(years=+0, months=+2))) # 2015-05-01 00:00:00

# Check that it handles "normal" scenarios
print(add_time(datetime(year=2015, month=6, day=19), relativedelta(months=+1))) # 2015-07-19 00:00:00
print(add_time(datetime(year=2015, month=6, day=30), relativedelta(years=+2, months=+1))) # 2017-07-30 00:00:00

# Check across years
print(add_time(datetime(year=2015, month=12, day=25), relativedelta(months=+1))) # 2016-01-25 00:00:00

# Check leap years
print(add_time(datetime(year=2016, month=1, day=29), relativedelta(years=+4, months=+1))) # 2020-02-29 00:00:00

Upvotes: 4

eumiro
eumiro

Reputation: 212835

This seems to work. It is quite clean, but not beautiful:

def add_month(now):
    try:
        then = (now + relativedelta(months=1)).replace(day=now.day)
    except ValueError:
        then = (now + relativedelta(months=2)).replace(day=1)
    return then

for now in [datetime(2015, 1, 20), datetime(2015, 1, 31), datetime(2015, 2, 28)]:
    print now, add_month(now)

prints:

2015-01-20 00:00:00 2015-02-20 00:00:00
2015-01-31 00:00:00 2015-03-01 00:00:00
2015-02-28 00:00:00 2015-03-28 00:00:00

It adds one month and tries to replace the day with the original day. If it succeeds, it is no special case. If it fails (ValueError), we have to add another month and go to its first day.

Upvotes: 2

venpa
venpa

Reputation: 4318

Quick solution:

import datetime
import calendar
dt = datetime.datetime(year=2014, month=5, day=2)
d = calendar.monthrange(dt.year,dt.month+1)[1] 
print dt+datetime.timedelta(days=d+1)

Output for the first input (year=2014, month=5, day=2):

2014-06-02 00:00:00

Output for the second input (year=2015, month=1, day=31):

2015-03-01 00:00:00

Upvotes: 0

Related Questions