135

I'd like to get a person's age from its birthday. now - birthday / 365 doesn't work, because some years have 366 days. I came up with the following code:

now = Date.today
year = now.year - birth_date.year

if (date+year.year) > now
  year = year - 1
end

Is there a more Ruby'ish way to calculate age?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mantas
  • 5,691
  • 6
  • 27
  • 24
  • 7
    I like this question because it highlights the idea that there are "more Ruby" and "less Ruby" ways of doing things. It's important not only to be logically correct (which you could be by copying the C# answer), but also stylistically correct. And Adinochestva's answer makes good use of Ruby idiom. – James A. Rosen May 04 '09 at 22:07
  • Can you please update the accepted answer to be @philnash's answer? – Shailendra Shukla Oct 28 '21 at 20:44

26 Answers26

430

I know I'm late to the party here, but the accepted answer will break horribly when trying to work out the age of someone born on the 29th February on a leap year. This is because the call to birthday.to_date.change(:year => now.year) creates an invalid date.

I used the following code instead:

require 'date'

def age(dob)
  now = Time.now.utc.to_date
  now.year - dob.year - ((now.month > dob.month || (now.month == dob.month && now.day >= dob.day)) ? 0 : 1)
end
philnash
  • 70,667
  • 10
  • 60
  • 88
  • 4
    Use this, not the check-marked one which can't handle leap years – Ben G Jan 09 '13 at 21:08
  • Why do you return 0 || 1 instead of `true` || `false` ? – kingsfoil Aug 18 '14 at 19:05
  • 2
    @alex0112 Because the result (0 or 1) of that admittedly confusing conditional is subtracted from the difference in years between now and the birthdate. It is intended to find out whether the person has had their birthday yet this year and if not, they are 1 year less old than the difference between the years. – philnash Aug 20 '14 at 11:54
  • 3
    @andrej now = Date.today works but note that it does not handle issues with Timezone. In Rails Date.today returns a date based on the system timezone. ActiveRecord returns a time based on your Apps configured timezone. If the system timezone is different from your application timezone you would effectively be comparing time from two different timezone which would not be very accurate. – Favourite Onwuemene Mar 17 '16 at 01:24
  • Is this really the simplest solution? – Marco Prins May 25 '16 at 11:11
  • Could use the spaceship operator instead the long conditional https://ruby-doc.org/core-2.3.3/Time.html#method-i-3C-3D-3E – Justin Dec 15 '16 at 20:20
  • You can further simplify the conditional by checking if current month and day are before dob month and day. Like this `today.month <= dob.month && today.day < dob.day ? 1 : 0` – Vlado Cingel Nov 07 '19 at 14:53
  • 1
    @VladoCingel That will return the wrong result if the current month is before the birthday month and the current day is before the birthday day, for example if today is 23 July and the birthday is 21 December. – michaeltwofish Jul 23 '20 at 06:00
53

I've found this solution to work well and be readable for other people:

    age = Date.today.year - birthday.year
    age -= 1 if Date.today < birthday + age.years #for days before birthday

Easy and you don't need to worry about handling leap year and such.

PJ.
  • 1,547
  • 1
  • 12
  • 16
  • 3
    This requires Rails (for age.years), but could be made not to require Rails if you did something like `Date.today.month < birthday.month or Date.today.month == birthday.month && Date.today.mday < birthday.mday`. – Chuck May 04 '09 at 20:36
  • You're right -- sorry I assumed Rails as the question was tagged with it. But yes, easily modified for Ruby only. – PJ. May 06 '09 at 14:28
  • 1
    I had chosen this one at first because it's the prettiest, but in production, it's frequently wrong, for reasons I don't fathom. The above one using Time.now.utc.to_date seems to be working better. – Kevin Nov 19 '11 at 21:23
  • @Kevin Interesting. I've never had a problem with this myself but that doesn't mean there isn't one. Could you give me a specific example? I'd like to know if there's a bug. Thanks. – PJ. Nov 21 '11 at 19:16
  • This is the most readable solution and works fine. I have a bunch of specs running against this method and they all continue to pass. This solution gets my thumbs up for being most ruby-like in its simplicity. – Joost Baaij Mar 09 '12 at 20:53
  • This breaks for leap day birthdays. If birthday is 2004-02-29, and today is 2014-02-28, age should be 9. With this method, it is ten, because `birthday + 10.years` calculates into 2014-02-28. There is no single “correct” implementation of Date#+ when the result is a leap day in a non leap-year. – pusillanimous Mar 19 '14 at 09:52
  • 3
    @sigvei - that's a feature, not a bug ;) In most countries, including the US, the 28th is legally considered your birthday in a non-leap year if you are a leap baby. The person would indeed be considered 10. – PJ. Jul 31 '14 at 16:38
  • Just for completeness - the variable needs to be explicitly returned or an unexpected nil may get returned instead. – silverdr Aug 26 '16 at 23:58
33

Use this:

def age
  now = Time.now.utc.to_date
  now.year - birthday.year - (birthday.to_date.change(:year => now.year) > now ? 1 : 0)
end
Spoike
  • 119,724
  • 44
  • 140
  • 158
Sadegh
  • 6,654
  • 4
  • 34
  • 44
  • 41
    This breaks if birthday.to_date is a leap year and the current year isn't. Not a big occurrence, but it's been causing me problems. – philnash Mar 01 '10 at 16:58
  • 1
    Downvoting to encourage philnash's answer. – Nick Sonneveld Oct 01 '14 at 07:33
  • 2
    Another reason to prefer [philnash's answer](http://stackoverflow.com/a/2357790/162793), is that it works with plain old Ruby, while the accepted answer only works with `rails/activesupport`. – sheldonh Mar 02 '15 at 12:21
17

One liner in Ruby on Rails (ActiveSupport). Handles leap years, leap seconds and all.

def age(birthday)
  (Time.now.to_fs(:number).to_i - birthday.to_time.to_fs(:number).to_i)/10e9.to_i
end

Logic from here - How do I calculate someone's age based on a DateTime type birthday?

Assuming both dates are in same timezone, if not call utc() before to_fs() on both.

Andrew
  • 2,829
  • 3
  • 22
  • 19
Vikrant Chaudhary
  • 11,089
  • 10
  • 53
  • 68
9
(Date.today.strftime('%Y%m%d').to_i - dob.strftime('%Y%m%d').to_i) / 10000
pguardiario
  • 53,827
  • 19
  • 119
  • 159
  • So this essentially calculates the difference in days if each month was 100 days long, and each year was 100 months long. Which doesn't make a difference if you only keep the year part. – Jonathan Allard Jul 25 '18 at 18:42
7

My suggestion:

def age(birthday)
    ((Time.now - birthday.to_time)/(60*60*24*365)).floor
end

The trick is that the minus operation with Time returns seconds

jlebrijo
  • 521
  • 5
  • 7
  • This is almost right. Leap years mean that a year is actually 365.25 days long. That also means that at best, this method might not increment your age until 18 hours into your birthday. – Ryan Lue Jun 21 '17 at 15:05
7

The answers so far are kinda weird. Your original attempt was pretty close to the right way to do this:

birthday = DateTime.new(1900, 1, 1)
age = (DateTime.now - birthday) / 365.25 # or (1.year / 1.day)

You will get a fractional result, so feel free to convert the result to an integer with to_i. This is a better solution because it correctly treats the date difference as a time period measured in days (or seconds in the case of the related Time class) since the event. Then a simple division by the number of days in a year gives you the age. When calculating age in years this way, as long as you retain the original DOB value, no allowance needs to be made for leap years.

Bob Aman
  • 32,839
  • 9
  • 71
  • 95
  • birthday = Time.mktime(1960,5,5) gives me out of range (epoch problems?) – Andrew Grimm May 05 '09 at 00:18
  • Yeah, go go epoch issues. I've updated the answer to resolve this. – Bob Aman May 06 '09 at 17:37
  • `birthday = DateTime.now - 1.year` gives me an age of 0. Unfortunately, dividing by 365.25 is a little imprecise. – Samir Talwar May 06 '09 at 18:06
  • You can't subtract 1.year like that from a DateTime object. 1.year resolves to the number of seconds in a year. DateTime objects operate based on days. For example: (DateTime.now - 365.25).strftime("%D") As for precision, if you're really just dealing with birthdays, it's plenty precise. Fact of the matter is, people are already quite imprecise when it comes to ages. We're born at a precise moment in time, but we don't usually give the exact hour, minute, and second of our birth when we write down our DOB. My argument here is that you really don't want to do this calculation manually. – Bob Aman May 06 '09 at 18:59
  • 1
    This doesn't work for people born before 1900. For example Gertrude Baines is reported as having an age of 114.9979 on her birthday in 2009. – Andrew Grimm May 07 '09 at 03:24
  • Then you've done your math wrong. (DateTime.now - DateTime.new(1800, 1, 1)).to_f / 365.25 #=> 209.357540666406 – Bob Aman May 12 '09 at 20:13
  • Oh, my bad, I missed what you were getting at. Didn't read all the way through, thought you meant a problem with out of range dates. Yes, that does happen. But it also happens for any algorithm involving time subtraction. You can compensate by rounding at certain thresholds, however, I'm inclined to say that you're working too hard at that point. – Bob Aman May 12 '09 at 20:17
  • Time seems to be reimplemented for ruby 1.9.2, so hopefully it won't have epoch issues. – Andrew Grimm Jul 25 '09 at 06:35
  • Oh, nice. I certainly hope so. – Bob Aman Jul 27 '09 at 19:36
  • This method can be off by a day if you happen to care. This is due to how you are flooring the number with .to_i. Today is Feb 9th. If you set your birthday to today this method will not compute you as being one year older until tomorrow. – Jared Brown Feb 10 '11 at 01:31
  • Hmm, Jared's correct. It appears that this works or doesn't work depending on what year it is. I'll debug and update the answer. – Bob Aman Feb 10 '11 at 23:03
  • Huh, well, I assumed that `DateTime` was implemented correctly, but it looks like it assumes 365 days in all years. Dividing by `365.0` appears to produce the right number. – Bob Aman Feb 11 '11 at 04:39
  • Doesn't work for all cases. For example: ('2012-02-01'.to_date - '1990-02-01'.to_date)/365.25 => 21.998631074606433 (expected 22) – Tyler Rick May 04 '12 at 18:04
  • 365.25 is the average length of a year. That's why @TylerRick is having his issue. This gets you much closer to the actual answer, but is not correct in all cases. – Preston Marshall Sep 23 '13 at 19:25
5

This answer is the best, upvote it instead.


I like @philnash's solution, but the conditional could be compacter. What that boolean expression does is comparing [month, day] pairs using lexicographic order, so one could just use ruby's string comparison instead:

def age(dob)
  now = Date.today
  now.year - dob.year - (now.strftime('%m%d') < dob.strftime('%m%d') ? 1 : 0)
end
artm
  • 3,559
  • 1
  • 26
  • 36
5

I like this one:

now = Date.current
age = now.year - dob.year
age -= 1 if now.yday < dob.yday
tpalm
  • 75
  • 1
  • 1
  • If you think this is a reasonable contender to a 3yo question that already has 10 other answers, you should include more reasons than personal preference. Otherwise, you won't get much attention – John Dvorak Feb 15 '13 at 18:33
  • 2
    this breaks when one year is a leap year and another isn't – artm Feb 23 '13 at 09:29
  • If last yr feb's got 29 days, this calculation will fail – phil88530 Jun 19 '13 at 10:18
  • At first glance this looks clever and succinct, but is inaccurate because the `yday` (day of year) of dates after February differ in leap years. Eg `Date.parse("2000-03-01").yday => 61`, while `Date.parse("2001-03-01").yday => 60` – David Cook Sep 03 '21 at 03:46
4

This is a conversion of this answer (it's received a lot of votes):

# convert dates to yyyymmdd format
today = (Date.current.year * 100 + Date.current.month) * 100 + Date.today.day
dob = (dob.year * 100 + dob.month) * 100 + dob.day
# NOTE: could also use `.strftime('%Y%m%d').to_i`

# convert to age in years
years_old = (today - dob) / 10000

It's definitely unique in its approach but makes perfect sense when you realise what it does:

today = 20140702 # 2 July 2014

# person born this time last year is a 1 year old
years = (today - 20130702) / 10000

# person born a year ago tomorrow is still only 0 years old
years = (today - 20130703) / 10000

# person born today is 0
years = (today - 20140702) / 10000  # person born today is 0 years old

# person born in a leap year (eg. 1984) comparing with non-leap year
years = (20140228 - 19840229) / 10000 # 29 - a full year hasn't yet elapsed even though some leap year babies think it has, technically this is the last day of the previous year
years = (20140301 - 19840229) / 10000 # 30

# person born in a leap year (eg. 1984) comparing with leap year (eg. 2016)
years = (20160229 - 19840229) / 10000 # 32
Community
  • 1
  • 1
br3nt
  • 9,017
  • 3
  • 42
  • 63
2

Because Ruby on Rails is tagged, the dotiw gem overrides the Rails built-in distance_of_times_in_words and provides distance_of_times_in_words_hash which can be used to determine the age. Leap years are handled fine for the years portion although be aware that Feb 29 does have an effect on the days portion that warrants understanding if that level of detail is needed. Also, if you don't like how dotiw changes the format of distance_of_time_in_words, use the :vague option to revert to the original format.

Add dotiw to the Gemfile:

gem 'dotiw'

On the command line:

bundle

Include the DateHelper in the appropriate model to gain access to distance_of_time_in_words and distance_of_time_in_words_hash. In this example the model is 'User' and the birthday field is 'birthday.

class User < ActiveRecord::Base
  include ActionView::Helpers::DateHelper

Add this method to that same model.

def age
  return nil if self.birthday.nil?
  date_today = Date.today
  age = distance_of_time_in_words_hash(date_today, self.birthday).fetch("years", 0)
  age *= -1 if self.birthday > date_today
  return age
end

Usage:

u = User.new("birthday(1i)" => "2011", "birthday(2i)" => "10", "birthday(3i)" => "23")
u.age
mindriot
  • 14,149
  • 4
  • 29
  • 40
1

I believe this is functionally equivalent to @philnash's answer, but IMO more easily understandable.

class BirthDate
  def initialize(birth_date)
    @birth_date = birth_date
    @now = Time.now.utc.to_date
  end

  def time_ago_in_years
    if today_is_before_birthday_in_same_year?
      age_based_on_years - 1
    else
      age_based_on_years
    end
  end

  private

  def age_based_on_years
    @now.year - @birth_date.year
  end

  def today_is_before_birthday_in_same_year?
    (@now.month < @birth_date.month) || ((@now.month == @birth_date.month) && (@now.day < @birth_date.day))
  end
end

Usage:

> BirthDate.new(Date.parse('1988-02-29')).time_ago_in_years
 => 31 
Jason Swett
  • 43,526
  • 67
  • 220
  • 351
1
class User
  def age
    return unless birthdate
    (Time.zone.now - birthdate.to_time) / 1.year
  end
end

Can be checked with the following test:

RSpec.describe User do
  describe "#age" do
    context "when born 29 years ago" do
      let!(:user) { create(:user, birthdate: 29.years.ago) }

      it "has an age of 29" do
        expect(user.age.round).to eq(29)
      end
    end
  end
end
Dorian
  • 7,749
  • 4
  • 38
  • 57
0

The following seems to work (but I'd appreciate it if it was checked).

age = now.year - bday.year
age -= 1 if now.to_a[7] < bday.to_a[7]
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
testr
  • 453
  • 3
  • 5
0

If you don't care about a day or two, this would be shorter and pretty self-explanitory.

(Time.now - Time.gm(1986, 1, 27).to_i).year - 1970
hakunin
  • 4,041
  • 6
  • 40
  • 57
0

Ok what about this:

def age
  return unless dob
  t = Date.today
  age = t.year - dob.year
  b4bday = t.strftime('%m%d') < dob.strftime('%m%d')
  age - (b4bday ? 1 : 0)
end

This is assuming we are using rails, calling the age method on a model, and the model has a date database column dob. This is different from other answers because this method uses strings to determine if we are before this year's birthday.

For example, if dob is 2004/2/28 and today is 2014/2/28, age will be 2014 - 2004 or 10. The floats will be 0228 and 0229. b4bday will be "0228" < "0229" or true. Finally, we will subtract 1 from age and get 9.

This would be the normal way to compare the two times.

def age
  return unless dob
  t = Date.today
  age = today.year - dob.year
  b4bday = Date.new(2016, t.month, t.day) < Date.new(2016, dob.month, dob.day)
  age - (b4bday ? 1 : 0)
end

This works the same, but the b4bday line is too long. The 2016 year is also unnecessary. The string comparison at the beginning was the result.

You can also do this

Date::DATE_FORMATS[:md] = '%m%d'

def age
  return unless dob
  t = Date.today
  age = t.year - dob.year
  b4bday = t.to_s(:md) < dob.to_s(:md)
  age - (b4bday ? 1 : 0)
end

If you aren't using rails, try this

def age(dob)
  t = Time.now
  age = t.year - dob.year
  b4bday = t.strftime('%m%d') < dob.strftime('%m%d')
  age - (b4bday ? 1 : 0)
end

Cruz Nunez
  • 2,949
  • 1
  • 23
  • 33
  • Just fyi, your answer is logically the same as philnash's. His answer is much cleaner and doesn't rely on Rails though. – Mantas Dec 04 '16 at 12:06
  • @Mantas Philnash said he used that method in a Rails project. You had rails in the tags. His method has two more comparisons than mine. The last line of his method is hard to understand. – Cruz Nunez Dec 06 '16 at 09:25
  • sorry, but philnash's code is much cleaner than yours. Also, his code is a simple method. Yours depend on having "dob" value. Which may be not that clear to new users. And not even helps the code much. Yes, your very last sample gets rid of it. But philnash's is just perfect keep-it-simple-stupid piece of code. – Mantas Dec 07 '16 at 10:16
0

I think it's alot better to do not count months, because you can get exact day of a year by using Time.zone.now.yday.

def age
  years  = Time.zone.now.year - birthday.year
  y_days = Time.zone.now.yday - birthday.yday

  y_days < 0 ? years - 1 : years
end
0

Came up with a Rails variation of this solution

def age(dob)
    now = Date.today
    age = now.year - dob.year
    age -= 1 if dob > now.years_ago(age)
    age
end
derosm2
  • 138
  • 1
  • 5
0

DateHelper can be used to get years only

puts time_ago_in_words '1999-08-22'

almost 20 years

hsul4n
  • 491
  • 7
  • 15
0
  def computed_age
    if birth_date.present?
      current_time.year - birth_date.year - (age_by_bday || check_if_newborn ? 0 : 1)
    else
      age.presence || 0
    end
  end


  private

  def current_time
    Time.now.utc.to_date
  end

  def age_by_bday
    current_time.month > birth_date.month
  end

  def check_if_newborn
    (current_time.month == birth_date.month && current_time.day >= birth_date.day)
  end```
0
(Date.today - birth_date).days.seconds.in_years.floor

In Ruby on Rails (thanks to ActiveSupport), there are many ways to solve this problem.

First of all, some clarifications:

  • The difference between two 'Date' returns the number of days
  • The difference between two 'Time' returns the number of seconds
  • in_years() returns the amount of years a duration covers as a float
  • 1.year is equivalent to 365.2425.days.seconds

ActiveSupport constants/methods are more accurate than a "simple" calculation of seconds in a year

1.year.seconds    # => 31556952
365.25*24*60*60   # => 31557600.0
365*24*60*60      # => 31536000

So, if you work with Date, you can do :

(Date.today - birth_date).days.seconds.in_years.floor
# or this is also a good way
((Date.today - birth_date).days / 1.year).floor

Note the use of floor method to convert the Float in Integer

But you can also use Time, like this :

(Time.now - birth_date.to_time).seconds.in_years.floor
((Time.now - birth_date.to_time) / 1.year).floor

If you want to use only plain ruby, I suggest this answer:

SECONDS_PER_YEAR = 31556952
SECONDS_PER_DAY  = 86400

((Date.today - birth_date) * SECONDS_PER_DAY / SECONDS_PER_YEAR).floor
# or
((Time.now - birth_date.to_time) / SECONDS_PER_YEAR).floor
Florent
  • 1
  • 1
-1

To account for leap years (and assuming activesupport presence):

def age
  return unless birthday
  now = Time.now.utc.to_date
  years = now.year - birthday.year
  years - (birthday.years_since(years) > now ? 1 : 0)
end

years_since will correctly modify the date to take into account non-leap years (when birthday is 02-29).

Vitaly Kushner
  • 9,247
  • 8
  • 33
  • 41
-1
  def birthday(user)
    today = Date.today
    new = user.birthday.to_date.change(:year => today.year)
    user = user.birthday
    if Date.civil_to_jd(today.year, today.month, today.day) >= Date.civil_to_jd(new.year, new.month, new.day)
      age = today.year - user.year
    else
      age = (today.year - user.year) -1
    end
    age
  end
Brian
  • 1
-1

Here's my solution which also allows calculating the age at a specific date:

def age on = Date.today
  (_ = on.year - birthday.year) - (on < birthday.since(_.years) ? 1 : 0)
end
hurikhan77
  • 5,881
  • 3
  • 32
  • 47
-1
Time.now.year - self.birthdate.year - (birthdate.to_date.change(:year => Time.now.year) > Time.now.to_date ? 1 : 0)
-2

I had to deal with this too, but for months. Became way too complicated. The simplest way I could think of was:

def month_number(today = Date.today)
  n = 0
  while (dob >> n+1) <= today
    n += 1
  end
  n
end

You could do the same with 12 months:

def age(today = Date.today)
  n = 0
  while (dob >> n+12) <= today
    n += 1
  end
  n
end

This will use Date class to increment the month, which will deal with 28 days and leap year etc.

Mooktakim Ahmed
  • 1,021
  • 1
  • 10
  • 17