81

Is there a more efficient way of doing this below? I want to have the difference in years between two dates as a single scalar. Any suggestions are welcome.

from datetime import datetime
start_date = datetime(2010,4,28,12,33)
end_date = datetime(2010,5,5,23,14)
difference  = end_date - start_date
difference_in_years = (difference.days + difference.seconds/86400)/365.2425
c00kiemonster
  • 20,805
  • 33
  • 89
  • 129
  • In what sense are you hoping it could be "more efficient"? Faster? Less code? – Karl Knechtel Dec 14 '10 at 07:58
  • 10
    Be explicit; use floating point constants instead of integer constants. Your last line needs to be `difference_in_years = (difference.days + difference.seconds/86400.0)/365.2425` to give the expected answer when run with Python 2.X. – John Machin Dec 14 '10 at 08:09
  • @John Machin Good point, didn't think of that. – c00kiemonster Dec 14 '10 at 08:21
  • @ Karl Knechtel It would be very nice with a .years attribute that would return the same value as my `difference_in_years` for the timedelta object. But it doesn't really fit into the timedelta implementation, and I don't really think there is a need for one. – c00kiemonster Dec 14 '10 at 08:26
  • and it's not really worth subclassing the timedelta object to add one. – dan_waterworth Dec 14 '10 at 09:36
  • What's wrong with the above? It's very nice. – S.Lott Dec 14 '10 at 12:26
  • Nothing wrong with it really. If it's meant to be run once in a script I wouldn't give it much thought, but when it is run in an inner loop it's definitely worthwhile to explore ways to make it more efficient. The timedelta is for example very handy when calculating difference between dates. I am just wondering whether my particular code could be optimized further... – c00kiemonster Dec 14 '10 at 13:58
  • @John Machin: use `from __future__ import division` instead, and you have a division operator that works with integers as well. – Lennart Regebro Dec 15 '10 at 22:03
  • You forgot to include microseconds. :) – Lennart Regebro Dec 15 '10 at 22:06
  • @Lennart Regebro: With the .0 on the end of the integer, the code is immediately stand-alone-unambiguous i.e. without having to wonder what version of Python is being run and what mumbo-jumbo has been included at the top of the module. – John Machin Dec 15 '10 at 22:56
  • You don't have to wonder what version is run, that was introduced in version 2.2. – Lennart Regebro Dec 15 '10 at 23:11
  • If no mumbo-jumbo, version-wondering is still required. – John Machin Dec 15 '10 at 23:35
  • I have an update which I hope is useful. – Mark Ransom Dec 19 '10 at 05:54
  • 1
    NASA ```https://pumas.nasa.gov/files/04_21_97_1.pdf``` said in 1997 that there are 365.2422 days in a year. – Marichyasana Feb 25 '21 at 18:11
  • Yours is still the best answer when you want fractional years. – ChaimG Oct 28 '21 at 17:02

15 Answers15

121

If you want precise results, I recommend using the dateutil library.

from dateutil.relativedelta import relativedelta
difference_in_years = relativedelta(end_date, start_date).years

This is for complete years (e.g. a person's age). If you want fractional years, then add months, days, hours, ... up to the desired precision.

Karl Bartel
  • 2,929
  • 1
  • 25
  • 27
  • This gives me negative results if the dates are switched... anyway to correct this behaviour? – Zubo Jul 17 '17 at 09:53
  • @zubo you can use this oneliner before calling the relativedelta function: `end_date, start_date = (end_date, start_date) if end_date > start_date else (start_date, end_date)` – Doody P Aug 07 '17 at 14:37
  • 2
    Just put abs() around the result. – Mark Chackerian Oct 10 '18 at 15:19
  • For fractional years this is not a good answer. Because how many years do you add per month, day, hour ...? – ChaimG Oct 28 '21 at 16:56
  • 1
    @ChaimG That really depends on what you mean by a fraction of a year. Is one January difference 1/12th of a year because it is one month or is it 31/365th of a (non-leap) year because you look at the number of days? Both answers are valid depending on context. – Karl Bartel Oct 29 '21 at 07:44
  • The OP's definition is fine. – ChaimG Oct 31 '21 at 00:05
22

I use one of these to calculate person's age:

import datetime
dob = datetime.date(1980, 10, 10)

def age():
    today = datetime.date.today()
    years = today.year - dob.year
    if today.month < dob.month or (today.month == dob.month and today.day < dob.day):
        years -= 1
    return years

def age2():
    today = datetime.date.today()
    this_year_birthday = datetime.date(today.year, dob.month, dob.day)
    if this_year_birthday < today:
        years = today.year - dob.year
    else:
        years = today.year - dob.year - 1
    return years
Dici
  • 23,827
  • 6
  • 36
  • 79
Kostyantyn
  • 4,773
  • 3
  • 31
  • 30
15

Just do this:

from dateutil.relativedelta import relativedelta

myBirthday = datetime.datetime(1983,5,20,0,0,0,0)
now = datetime.datetime.now()



difference = relativedelta(now, myBirthday)
print("My years: "+str(difference.years))
Michel Fernandes
  • 1,015
  • 9
  • 8
9

More efficient? No, but more correct, probably. But it depends on how correct you want to be. Dates are not trivial things.

Years do not have a constant length. Do you want the difference in leap years or normal years? :-) As you calculate you are always going to get a slightly incorrect answer. And how long is a day in years? You say 1/365.2425. Well, yeah, averaged over a thousand years, yeah. But otherwise not.

So the question doesn't really make much sense.

To be correct you have to do this:

from datetime import datetime
from calendar import isleap
start_date = datetime(2005,4,28,12,33)
end_date = datetime(2010,5,5,23,14)
diffyears = end_date.year - start_date.year
difference  = end_date - start_date.replace(end_date.year)
days_in_year = isleap(end_date.year) and 366 or 365
difference_in_years = diffyears + (difference.days + difference.seconds/86400.0)/days_in_year

In this case that's a difference of 0.0012322917425568528 years, or 0.662 days, considering that this is not a leap year.

(and then we are ignoring microseconds. Heh.)

Lennart Regebro
  • 158,668
  • 41
  • 218
  • 248
  • Both your answer and the op's answer become counterintuitive from the perspective that different **calendar years** have different lengths. This becomes more blatant when trying to figure out how long the difference is between two years with the day being the same. – Brian Dec 15 '10 at 22:23
  • Good point, that really clarifies what I try to say. With the 365.2425 calculation you'd get different answers from 1999-05-05 to 2000-05-05 than from 2000-05-05 to 2001-05-05. That's clearly wrong. Then you are not counting in years, but in days. – Lennart Regebro Dec 15 '10 at 22:29
  • And if we want to have even MORE fun, we can add timezones in the mix. – Lennart Regebro Dec 15 '10 at 22:35
  • Years-and-a-fraction is a nonsense. There is no such thing as a correct definition. – John Machin Dec 15 '10 at 23:07
  • Right. If you want "days" don't call them 365ths of a year. :) – Lennart Regebro Dec 15 '10 at 23:13
  • This slots in under the 'any suggestions are welcome' category like mark's comment did. And I appreciate the discourse. Date arithmetic is always fun. I should probably have mentioned the accuracy vs precision thing in the question, but then again, in that case I wouldn't have had as interesting answers... – c00kiemonster Dec 16 '10 at 03:33
  • @Just cOOkiemonster: Just calculate the difference in days instead and you'll be fine. :) But some points would be nice. Hint, hint. – Lennart Regebro Dec 16 '10 at 06:18
  • It fails at `difference = end_date - start_date.replace(end_date.year)` when start_date is feb 29th and end date is not a leap year – Saikumarch Jun 13 '19 at 08:47
6

To make sense of leap years, you are almost forced to break this into two parts: an integral number of years, and a fractional part. Both need to deal with leap years, but in different ways - the integral needs to deal with a starting date of February 29, and the fractional must deal with the differing number of days in a year. You want the fractional part to increment in equal amounts until it equals 1.0 at the next anniversary date, so it should be based on the number of days in the year after the end date.

Do you want your date range to include 1900 or 2100? Things get a little easier if you don't.


Edit: It has taken me a long time to reason this through. The basic problem is that calendar years are not a constant size, but you're coercing them to be constant by setting them to 1.0. Any solution you come up with is going to have anomalies because of this, and you're going to have to choose which anomalies you can live with. John Machin was right.

What's the difference between 2008-02-28 and 2009-02-28? Most people would agree that it should be exactly 1.0 years. How about the difference between 2008-03-01 and 2009-03-01? Again, most people would agree that it should be exactly 1.0 years. If you choose to represent a date as a year plus a fraction of a year based on the day, it is impossible to make both of these statements true. This is the case for your original code which assumed a day was 1/365.2425 of a year, or indeed for any code which assumes a constant fraction of a year per day, even if the size of a day accounts for the years which are leap years.

My assertion that you needed to break this down into integral years and fractional years was an attempt to get around this problem. If you treat each of the previous conditions as an integral year, all you have to do is decide on which fraction to assign to any number of days left over. The problem with this scheme is that you still can't make sense of (date2-date1)+date3, because the fraction can't be resolved back to a day with any consistency.

Thus I am proposing yet another encoding, based on each year containing 366 days whether it is a leap year or not. The anomalies will firstly be that there can't be a date which is exactly a year (or 2 or 3) from Feb. 29 - "Sorry Johnny, you don't get a birthday this year, there's no Feb. 29" isn't always acceptable. Second is that if you try to coerce such a number back to a date, you'll have to account for non-leap years and check for the special case of Feb. 29 and convert it, probably to Mar. 1.

from datetime import datetime
from datetime import timedelta
from calendar import isleap

size_of_day = 1. / 366.
size_of_second = size_of_day / (24. * 60. * 60.)

def date_as_float(dt):
    days_from_jan1 = dt - datetime(dt.year, 1, 1)
    if not isleap(dt.year) and days_from_jan1.days >= 31+28:
        days_from_jan1 += timedelta(1)
    return dt.year + days_from_jan1.days * size_of_day + days_from_jan1.seconds * size_of_second

start_date = datetime(2010,4,28,12,33)
end_date = datetime(2010,5,5,23,14)
difference_in_years = date_as_float(end_time) - date_as_float(start_time)

I'm not suggesting that this is the solution, because I don't think a perfect solution is possible. But it has some desirable properties:

  • The difference between any dates with the same month and day and time will be an exact number of years.
  • Adding a difference to another date will result in a value that can be converted back into a useful date.
Mark Ransom
  • 286,393
  • 40
  • 379
  • 604
  • @Mark Ransom: You presume to tell the OP that he wants a baroque years-and-a-fraction gimmick instead of a simple one, without him saying what his application is, and without you saying to what use yours could be put? – John Machin Dec 15 '10 at 23:03
  • 1
    @John Machin, I wasn't trying to tell him what he needs, I hope I didn't come across that way. I took the "Any suggestions are welcome" to be an invitation to point out difficulties with his approach, whether they were relevant or not. The last sentence in my answer was worded harshly, but I added it at the last moment after reading some comments and I plead excessive haste. – Mark Ransom Dec 15 '10 at 23:10
  • @Mark Ransom: The point is that ANY approach to years-and-a-fraction that doesn't mention the use to which it will be put has problems, because there is no such thing as a constant-length year. Perhaps the OP is really happy to be counting in days, and is just dividing by 365.something to scale the numbers down to be a bit to be more intelligible to his audience ... – John Machin Dec 15 '10 at 23:30
  • @Mark, you are right, I am more interested in accuracy than precision. I appreciate your input, that was exactly the intention of the 'any suggestions are welcome' line... – c00kiemonster Dec 16 '10 at 03:25
  • @John Machin, after much consideration I determined that you are absolutely correct. I hope you can find something positive in my latest edit. – Mark Ransom Dec 19 '10 at 06:01
  • @Mark Ransom: Sorry to dash your unjustified hope. Your new bunch of code merely replaces one nonsense with another. It starts off badly with `size_of_day = 1. / 366.` and then gets worse. You don't say what purpose your "date encoding" fulfills. – John Machin Dec 19 '10 at 22:15
  • @c00kiemonster: What does "I am more interested in accuracy than precision" mean?? To what use are you going to put "difference in years between two dates as a single scalar"?? – John Machin Dec 19 '10 at 22:18
  • @John Machin: Accuracy vs precision (en.wikipedia.org/wiki/Accuracy_and_precision) is probably one of the few things I actually remember from my uni days, and it was told to me as an offhand remark by a professor and not as a part of any course work imagine that. In my case it means that the year parameter in the algorithm that uses it is fairly inelastic towards the algorithm's output so I don't need any more precision than the initial 5 lines of code. I knew from the start the intricacies of date arithmetic so that's why I slapped the 'Any suggestions are welcome' badboy onto the question. – c00kiemonster Dec 20 '10 at 06:01
  • @John Machin: I thought I made it clear, the source of the nonsense is replacing a variable size measurement with a fixed one. Your only choice is to find nonsense that has the properties you're looking for. My rationale was fully spelled out in the answer. – Mark Ransom Dec 20 '10 at 14:36
  • "But it has some desirable properties" Same was my solution then. My solution has one bug, though which your avoids. And I'm not entirely sure why we get different results, but also can't be bothered to find out. – Lennart Regebro Dec 21 '10 at 07:25
2

Here's a spin off of what Kostyantyn posted in his "age2" function. It's slightly shorter/cleaner and uses the traditional/colloquial meaning of an "age" or difference in years as well:

def ageInYears( d ):
    today = datetime.date.today()
    currentYrAnniversary = datetime.date( today.year, d.month, d.day )
    return (today.year - d.year) - (1 if today < currentYrAnniversary else 0)
BuvinJ
  • 9,113
  • 5
  • 74
  • 84
2

Since we're coming to the end of 2018...

from dateutil import parser
from dateutil.relativedelta import relativedelta

rip = [
    ["Tim Bergling\t\t",         " 8 Sep 1989", "20 Apr 2018"], # Avicii Swedish musician
    ["Stephen Hillenburg\t",     "21 Aug 1961", "26 Nov 2018"], # Creator of Spongebob
    ["Stephen Hawking\t\t",      " 8 Jan 1942", "14 Mar 2018"], # Theoretical physicist
    ["Stan Lee\t\t",             "28 Dec 1922", "12 Nov 2018"], # American comic book writer
    ["Stefán Karl Stefánsson\t", "10 Jul 1975", "21 Aug 2018"]  # Robbie Rotten from LazyTown
    ]

for name,born,died in rip:
    print("%s %s\t %s\t died at %i"%(name,born,died,relativedelta(parser.parse(died),parser.parse(born)).years))

output

Tim Bergling              8 Sep 1989     20 Apr 2018     died at 28
Stephen Hillenburg       21 Aug 1961     26 Nov 2018     died at 57
Stephen Hawking           8 Jan 1942     14 Mar 2018     died at 76
Stan Lee                 28 Dec 1922     12 Nov 2018     died at 95
Stefán Karl Stefánsson   10 Jul 1975     21 Aug 2018     died at 43
Puddle
  • 2,575
  • 1
  • 17
  • 32
2

I think what you're looking for is:

difference_in_years = difference.dt.days / 365.25
Dharman
  • 26,923
  • 21
  • 73
  • 125
Will
  • 37
  • 1
0

If you mean efficient in terms of code space then no, that's about the most efficient way to do that.

dan_waterworth
  • 6,091
  • 1
  • 27
  • 40
0

Here's what I came up with, without using an external dependency:

def year_diff(d1, d2):
    """Returns the number of years between the dates as a positive integer."""
    later = max(d1, d2)
    earlier = min(d1, d2)

    result = later.year - earlier.year
    if later.month < earlier.month or (later.month == earlier.month and later.day < earlier.day):
        result -= 1

    return result
Aidan Feldman
  • 4,793
  • 35
  • 46
  • This doesn't take into account leaf year (ex `year_diff('2016/02/29', '2019/02/28')`) – Metariat Jul 17 '19 at 16:29
  • 1
    @Metariat, I am confused by your comment. For the input that you suggested, Aidan Feldman's solution returns 2 (years). What answer do you believe is correct? Why? – Barzee May 04 '20 at 16:44
0

More robust function - calculates difference in years (age) and days:

def get_diff_in_years_and_days(from_date, to_date):
    try:
        from_in_this_year = date(to_date.year, from_date.month, from_date.day)
    except:
        from_in_this_year = date(to_date.year, from_date.month, from_date.day-1) # today is feb in leap year

    if from_in_this_year <= to_date:
        years = to_date.year - from_date.year
        days = (to_date - from_in_this_year).days
    else:
        years = to_date.year - from_date.year - 1
        try:
            from_in_prev_year = date(to_date.year-1, from_date.month, from_date.day)
        except:
            from_in_prev_year = date(to_date.year-1, from_date.month, from_date.day-1) # today is feb in leap year
        days = (to_date - from_in_prev_year).days

    assert days>=0 and days<=365, days
    assert years>=0, years

    return years, days

some unit-tests:

self.assertEqual((0,  0), get_diff_in_years_and_days(date(2018,1, 1), date(2018,1, 1)))
self.assertEqual((1,  0), get_diff_in_years_and_days(date(2017,1, 1), date(2018,1, 1)))
self.assertEqual((1,  1), get_diff_in_years_and_days(date(2017,1, 1), date(2018,1, 2)))
self.assertEqual((2,  0), get_diff_in_years_and_days(date(2016,2,29), date(2018,2,28)))
self.assertEqual((2,  1), get_diff_in_years_and_days(date(2014,2,28), date(2016,2,29)))
self.assertEqual((1,364), get_diff_in_years_and_days(date(2014,2,28), date(2016, 2,27)))
self.assertEqual((3,30) , get_diff_in_years_and_days(date(2015,10,1), date(2018,10,31)))
self.assertEqual((10,30), get_diff_in_years_and_days(date(2010,10,1), date(2020,10,31)))
self.assertEqual((3,31) , get_diff_in_years_and_days(date(2015,10,1), date(2018,11, 1)))
self.assertEqual((2,364), get_diff_in_years_and_days(date(2015,10,1), date(2018, 9,30)))
Robert Lujo
  • 14,190
  • 5
  • 53
  • 67
0

Before install library :

choco upgrade Python -y
python pip install python-dateutil

In One line in Python in Cmder (windows) :

python -c "import datetime; from dateutil.relativedelta import relativedelta; myBirthday = datetime.datetime(2019,2,6,11,0,0,0); now = datetime.datetime.utcnow(); diff = relativedelta(now, myBirthday); print ("'My'+'" "'+'year'+'" "'+':'+'" "'+'%d''" "''and''" "''%d''" "''microseconds'" % (diff.years, diff.microseconds))"

In Batch escape percent with %% :

python -c "import datetime; from dateutil.relativedelta import relativedelta; myBirthday = datetime.datetime(2019,2,6,11,0,0,0); now = datetime.datetime.utcnow(); diff = relativedelta(now, myBirthday); print ("'My'+'" "'+'year'+'" "'+':'+'" "'+'%%d''" "''and''" "''%%d''" "''microseconds'" %% (diff.years, diff.microseconds))"
jhonitalia
  • 69
  • 8
0

An update to BuvinJ solution which was an update to the Kostyantyn solution, age2().

Those functions fail if the date of birth is on leap day and the current year is not a leap year. The datetime library will throw an exception when trying to create 'currentYrAnniversary'.

Instead try,

from datetime import date

def ageInYears( d ):
    today = datetime.date.today()

    notQuiteYourBirthday = (today.month, today.day) < (born.month, born.day)
    return (today.year - d.year) - notQuiteYourBirthday
brocla
  • 63
  • 6
0

If you have the DOB as a string already you can do this:

from datetime import datetime as dt

def get_age(dob_str):
    now_str = dt.strftime(dt.utcnow(), '%Y-%m-%d')
    return int(now_str[:4]) - int(dob_str[:4]) - int(dob_str[5:] > now_str[5:])

Or if you want to write some unit tests set now_str as a named parameter:

from datetime import datetime as dt

def get_age(dob_str, now_str=dt.strftime(dt.utcnow(), '%Y-%m-%d')):
    return int(now_str[:4]) - int(dob_str[:4]) - int(dob_str[5:] > now_str[5:])
0

The simplest solution

from datetime import datetime

my_birthday = datetime(2000, 1, 1, 0, 0, 0, 0 )

now = datetime.now()

age = now.year - my_birthday.year

print(age)
MIM
  • 1