26

I am doing some time calculations in Python.

Goal:

Part of this is trying to :

Given a date, add time interval (X years, X months, X weeks), return date

ie

  • input args: input_time (datetime.date), interval (datetime.timedelta)
  • return: datetime.date

I looked at the datetime and datetime.timedelta docs

class datetime.timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)¶.

These seem to work well if I want to add a certain number of hours or weeks. However,

Problem:

  • I am trying to implement an operation such as date + 1 year and can't figure it out

E.g.

start = datetime.datetime(2000, 1, 1)
# expected output: datetime.datetime(2001, 1, 1)


# with the weeks, etc arguments given in timedelta, this fails unsurprisingly e.g 
start + datetime.timedelta(weeks = 52)

# returns datetime.datetime(2000, 12, 30, 0, 0)

Question

  • Is this year-based operation possible with the basic tools of datetime - if so, how would I go about it?

  • I realize that for the year example, I could just do start.replace(year = 2001), but that approach will fail if I have months or weeks as input.

  • From my understanding, the dateutil library has more advanced features, but I was not clear how well it interacts with the in-built datetime objects.

I have reviewed this similar question but it did not help me with this.

Any help is much appreciated!

Running Python 3.6.5 on MacOs.

patrick
  • 4,455
  • 6
  • 44
  • 61

4 Answers4

47

timedelta does not support years, because the duration of a year depends on which year (for example, leap years have Feb 29).

You could use a relativedelta instead (from PyPI package python-dateutil) which does support years and takes into account the baseline date for additions.

>>> from dateutil.relativedelta import relativedelta
>>> import datetime
>>> d = datetime.date(2020, 2, 29)
>>> d
datetime.date(2020, 2, 29)
>>> d + relativedelta(years=1)
datetime.date(2021, 2, 28)
Flimm
  • 136,138
  • 45
  • 251
  • 267
wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    thanks, this looks really good & I figured re the part with leap years...Looking at the [docs](https://dateutil.readthedocs.io/en/stable/examples.html#relativedelta-examples) for relativdelta, they consistently use the syntax `years=+1` , where you use `years=1`. Testing out, they seem to do the same thing, or is there a difference between the two? – patrick Jan 28 '19 at 01:13
  • 2
    @patrick probably too late to your comment about the syntax `years=+1`; it means going forward or back, so if you had to go back one year, you would do `years=-1`. From the docs: `Relative information, may be negative (argument is plural); adding or subtracting a relativedelta with relative information performs the corresponding aritmetic operation on the original datetime value with the information in the relativedelta.` – Adeel Siddiqui Nov 27 '19 at 07:29
14

You can hard code a new year value of the datetime using replace instead :)

This avoids leap years etc.

year_later = current.replace(year=current.year + 1)

Note that if the current date happens to be the 29th of February, this will raise a ValueError with the message: "Day is out of range for month". So you need to handle this special case, like this:

if current.month == 2 and current.day == 29:
    year_later = current.replace(year=current.year + 1, day=28)
else:
    year_later = current.replace(year=current.year + 1)
Flimm
  • 136,138
  • 45
  • 251
  • 267
enongad
  • 361
  • 1
  • 4
  • 3
    But what about leap years if current is Feb 29? – MDoe Aug 17 '20 at 15:22
  • 1
    @MDoe Good point. I've edited the answer to handle this special case. – Flimm Mar 10 '22 at 07:35
  • Just to be extremely pedantic for everyone's edification, you might not want 2025-02-28 for the "year ahead" date for 2024-02-29... depending on the use case you might consider that the "equivalent" year-ahead date is 2025-03-01! – mike rodent Oct 20 '22 at 18:53
6

My quick and dirty method is to use

y = [number of years]
timedelta(days= y * 365)

I found this question looking for a more elegant solution. For my uses a precise answer wasn't necessary. I don't mind losing a day each leap year in this particular case.

JDenman6
  • 329
  • 2
  • 8
0

My goal was very similar to yours. I wanted to have the same date, just on the next year. But i figured i can't avoid accounting for leap years. In the end it all boils down to what exactly is the requirement. Theoretically we can just add 365 days (and either loose a day when there was a Feb 29), or check in the next 365 days if there is a 29th of February, in which case add 1 more day. But checking that would have been complex, and in the end i used a simple check for the new date's day if it is different from the original and then add 1 more day. I understand with dateutil.relativedelta it is easier, but i wanted to do it without extra imports

demo:

from datetime import datetime, timedelta

dates = [datetime(1999, 1, 1),
         datetime(1999, 12, 31),
         datetime(2000, 1, 1),
         datetime(2019, 3, 1),
         datetime(2019, 1, 1),
         datetime(2020, 1, 1),
         datetime(2020, 3, 1),
         datetime(2020, 2, 29)
         ]
for date in dates:
    plus1year_date = date + timedelta(days=365)
    print(date, "\t - original date")
    print(plus1year_date, "\t - plus1year_date (+365 days)")
    if date.day != plus1year_date.day:
        plus1year_date = plus1year_date + timedelta(days=1)
        print(plus1year_date, "\t - plus1year_date adjusted for leap year")
    else:
        print("No need to adjust for leap year")
    print('--------------------------------------------------------------')

# Expected output:
# 1999-01-01 00:00:00    - original date
# 2000-01-01 00:00:00    - plus1year_date (+365 days)
# No need to adjust for leap year
# --------------------------------------------------------------
# 1999-12-31 00:00:00    - original date
# 2000-12-30 00:00:00    - plus1year_date (+365 days)
# 2000-12-31 00:00:00    - plus1year_date adjusted for leap year
# --------------------------------------------------------------
# 2000-01-01 00:00:00    - original date
# 2000-12-31 00:00:00    - plus1year_date (+365 days)
# 2001-01-01 00:00:00    - plus1year_date adjusted for leap year
# --------------------------------------------------------------
# ...

Edit: @MDoe - I forgot to mention that with the .year + 1 method one can end up with invalid dates if it is Feb 29 (ValueError). Also if somebody wants to add an interval that could include more than one leap year, then my code won't be correct.