Calculating the age given DOB

I guess the most generic solution would return the number of [years, months,
days] between two dates - so you can say "You are 31 years, 3 months and 4
days old"

Anyone can work out their own age in this format, so the algorithm should be
clear, albeit probably messy to implement.

···

On Fri, Mar 16, 2007 at 04:28:16AM +0900, Rick DeNatale wrote:

rick@frodo:/public/rubyscripts$ cat datemath.rb
#!/usr/math/bin/ruby

require 'date'
class Date

# return the number of days since the beginning of the year
def years_since(date)
   # The parens in the expression below aren't strictly necessary, but
   # I think it makes what's going on a little bit clearer.
   first, last = *(self >= date ? [date, self] : [self, date])
   (self <=> date) * ((last.year - first.year) - (first.yday >
last.yday ? 1 : 0))
end
end

Very nice piece of code Rick! I'm a Ruby newbee and I find it facinating
when I see code that applies better the "Ruby Way". Thanks for sharing.

I borrow parts of your code and rewrote the method I sent:

class Date
  def elapsedYearsAndDays(rangeDate)
    startDate, endDate = *(self >= rangeDate ? [rangeDate, self] :
[self, rangeDate])
    #This is the real problem: Febraury 29th!
    isMagicDate = (startDate.month == 2) && (startDate.day == 29)
    startDate += 1 if !endDate.leap? && isMagicDate
    elapsedYears = endDate.year - startDate.year
    previousStartDate = Date.new(endDate.year, startDate.month,
startDate.day)
    if endDate < previousStartDate
      elapsedYears -= 1
      previousStartDate = Date.new(endDate.year - 1, startDate.month,
startDate.day)
      previousStartDate -= 1 if previousStartDate.leap? && isMagicDate
    end
    return elapsedYears, endDate - previousStartDate
  end
end

startDates = [Date.today,
        Date.new(1963, 11, 22),
        Date.new(2007, 3, 1),
        Date.new(2004, 3, 1),
        Date.new(2004, 2, 29),
        Date.new(2004, 3, 1),
        Date.new(2004, 2, 29),
        Date.new(2007,3,15),
        Date.new(2000,3,14)]

endDates = [Date.new(1963, 11, 22),
      Date.new(1963, 11, 22),
      Date.new(2004, 3, 1),
      Date.new(2001, 3, 2),
      Date.new(2001, 3, 1),
      Date.new(2001, 3, 1),
      Date.new(2009, 3, 1),
      Date.new(2000,3,14),
      Date.new(2007,3,15)]

startDates.each_with_index do |startDate, index|
  elapsedYears, elapsedDays =
startDate.elapsedYearsAndDays(endDates[index])
  daysMessage = (elapsedDays > 0) ? (" and #{elapsedDays} day(s)") : ''
  puts "Start date: #{startDate} End date: #{endDates[index]}"
  puts "Elapsed time: #{elapsedYears} year(s)#{daysMessage}\n\n"
end

=begin

···

---------------------------------------------------------------------------
At the same time I took your code and added the same test dates I used:
=end

require 'date'
class Date

  # return the number of days since the beginning of the year
  def years_since(date)
    # The parens in the expression below aren't strictly necessary, but
    # I think it makes what's going on a little bit clearer.
    first, last = *(self >= date ? [date, self] : [self, date])
    (self <=> date) * ((last.year - first.year) - (first.yday >
last.yday ? 1 : 0))
  end
end

startDates = [Date.today,
        Date.new(1963, 11, 22),
        Date.new(2007, 3, 1),
        Date.new(2004, 3, 1),
        Date.new(2004, 2, 29),
        Date.new(2004, 3, 1),
        Date.new(2004, 2, 29),
        Date.new(2007,3,15),
        Date.new(2000,3,14)]

endDates = [Date.new(1963, 11, 22),
      Date.new(1963, 11, 22),
      Date.new(2004, 3, 1),
      Date.new(2001, 3, 2),
      Date.new(2001, 3, 1),
      Date.new(2001, 3, 1),
      Date.new(2009, 3, 1),
      Date.new(2000,3,14),
      Date.new(2007,3,15)]

startDates.each_with_index do |startDate, index|
  elapsedYears = startDate.years_since(endDates[index])
  puts "Start date: #{startDate} End date: #{endDates[index]}"
  puts "Elapsed time: #{elapsedYears} year(s)\n\n"
end

=begin
As you can see from the results, both pieces of code give the same
results
except when a leap year is involved. I think my version is correct in
those cases :slight_smile: because we don't want to say that a complete year has
passed until that is totally true.
Regards,

Nando
=end

--
Posted via http://www.ruby-forum.com/.

Rick DeNatale schrieb:

I think that this does the right thing:
(...)

   tryit(Date.new(2007,3,1), Date.new(2004,3,1))
   tryit(Date.new(2004,3,1), Date.new(2001,3,2))

# =>

   There are 2 years between 2004-03-01 and 2007-03-01
   There are 3 years between 2001-03-02 and 2004-03-01

See [ruby-talk:243700].

Regards,
Pit

Extending the original poster's question, as people here are aware
working in complete months in Gregorian calendars can be tricky. For
example, if you are trying to generate a series of dates at intervals
of an integer number of months (years are just 12 months) then - as
far as I'm aware - there's no "standard" way of doing it in Ruby, or
in many other computer languages for that matter. (I'd be delighted to
be contradicted: this also applies to anything else I've got wrong in
the following post.)

The Date method >> doesn't do it if the day of the month is greater than 28:
    require 'date'
    dt = Date.new( 2000, 1, 31 ) # 2000-01-31
    dt = dt >> 1 # 2000-02-29; which is correct
    dt = dt >> 1 # 2000-03-29; what is wanted is 2000-03-31
    dt = dt >> 1 # 2000-04-29; what is wanted is 2000-04-30
(Of course, you can use dt >> 1; dt >> 2; dt >> 3; etc, but it's not elegant.)

Also, date2 - date will give the number of days, but as far as I am
aware there isn't a "standard" Ruby way of calculating periods between
two dates in terms of, for example, complete months. (Or complete 12
months, the original poster's problem.)

If there isn't anything "standard" out there which does this sort of
thing, I'd be more than happy to collaborate on something if people
would find it useful. Or if there are one or more "projects" out there
which work, or are close to working, I'd be happy to collaborate on
something which could become "standard". Or indeed in porting
something from another language.

I have done a lot of this type of working with Gregorian dates in
various computer languages, but my Ruby experience is very limited,
and I'm not a programmer. (Even though I've been reading this list
since 2003.) Also, I sometimes feel that programming is too English
and Western oriented, so I'd be interested (as far as is reasonably
practical) in making things more general than just the Gregorian
calendar after the date(s) that the Julian calendar was corrected.

I wrote some code some time ago: the proper names can be decided on
later, but I have in mind adding methods to Date
  dt.to( dt2 ), dt2.from( dt )
both of which would return an instance of a Date_period class, which
would hold the number of days from dt to dt2, and also the number of
complete months and the number of days following the complete months.
(And the dates themselves - or rather their object references - for
various reasons, which might or might not be a good idea.) Date_period
would have methods to return days, complete years, years and days,
months and days, etc.

For a series of dates the idea is to sub-class or modify Date to have
an (optional?) instance variable @anniversary_day, which is needed to
be able to retain the "true" anniversary day as well as the day in the
month in the date instance. Or at least to add a Date method
step_with_anniversary, which would have an option for the anniversary
day to be 29, 30 or 31 even if the start date is 28 of February.

Depending on whether it's a good idea or not, + and - (and maybe <<
and >>) in Date (or the subclass) could be adapted to also work with
instances of Date_period, or new methods could be added to Date (or
the subclass).

There are some tricky problems to be solved, and this post is long
enough already, but I'd be more happy to correspond off list
(including sending copies of the code I have), and then report back.

Thanks, yep my bad!

Here's a refined attempt.

I've fixed that problem and I've also added support to specify a day
to be used as the anniversary date for a leap-day. I did extensive
research (on Wikipedia <G>) and it seems that:

  1) People born on February 29 are called leaplings.
  2) For legal purposes most jurisdictions consider 1 March to be the
birthdate of a leapling in non-leap years for the purposes of
determining legal age.
  3) There are some jurisdictions, e.g. Taiwan which use 28 February instead.

Although some leaplings try to pass themselves off as approximately
1/4 their legal age, I haven't made allowances for that in the
following code:

rick@frodo:/public/rubyscripts$ cat datemath.rb
require 'date'
class Date

  def leap_year?
    year % 400 == 0 || year % 100 != 0 && year % 4 == 0
  end

  def leap_day?
    month = 2 && day == 29
  end

  # The lyday is an altered yday. It is computed as if every year
  # was a leap year. It's purpose is to determine whether a date has
  # been 'virtually' crossed
  def lyday
    yday + ((leap_year? || yday < 60) ? 0 : 1)
  end

  # return the 'legal' anniversary day cooresponding to first in the
year of last
  # This is the first date unless that date is leap day (29 February
xxxx) and the
  # second date is within a leap year.

···

On 3/16/07, Pit Capitain <pit@capitain.de> wrote:

Rick DeNatale schrieb:
> I think that this does the right thing:
> (...)

   tryit(Date.new(2007,3,1), Date.new(2004,3,1))
   tryit(Date.new(2004,3,1), Date.new(2001,3,2))

# =>

   There are 2 years between 2004-03-01 and 2007-03-01
   There are 3 years between 2001-03-02 and 2004-03-01

  #
  # The leapling date is 1 March, xxxx by default. The year of the
leapling date is
  # ignored.
  def self.ly_adjust(first, second, leapling_date)
    if first.leap_day? && !second.leap_year?
      leapling_date ||= Date.new(2004,3,1)
      Date.new(first.year, leapling_date.month, leapling_date.day)
    else
      first
    end
  end

  # return the number of years since the given date.
  # leapling date is a date, (in any year) which is
  # considered the anniversary of February 29 in non leap years.
  # In most jurisdictions the legal birthday in non-leap years for determining
  # legal ages is March 1, which is the default, however some jurisdictions
  # legislate another date, most commonly February 28.
  def years_since(date,leapling_date = nil)
    first, last = *(self >= date ? [date, self] : [self, date])
    first = Date.ly_adjust(first,last, leapling_date)
    (self <=> date) * ((last.year - first.year) - (first.lyday >
last.lyday ? 1 : 0))
  end
end

# The following methods demonstrate and test the above code.
# These really should be Test::Unit test cases, but I think that this form works
# better for showing what the code does as well as verification.
#
# tryit displays the number of years between an end date and a start date
# for leapling_dates of nil, 1 March, and 28 February,
# it prints each result, and returns an array of the three results.
def tryit(start_date,end_date)
  leapling_dates = [nil]
  result =
  for ld in [nil, Date.new(2000,3,1), Date.new(2000,2,28)]
    puts "With leapling_date of #{ld}" if ld
    diff = end_date.years_since(start_date,ld)
    result << diff
    puts "There are #{diff} years between #{start_date} and #{end_date}"
  end
  puts
  result
end

# tryit2 takes two dates, and the array expected from tryit
# It calls tryit, checks the result and prints an error message if
# the results aren't as expected.
# It then reverses the arguments which should result in negated
# values of the expected results.
def tryit2(start_date,end_date,expected)
  puts "***** Error *****" unless expected == tryit(start_date, end_date)
  expected = expected.map {|e| -e}
  puts "***** Error *****" unless expected == tryit(end_date, start_date)
end

tryit2(Date.new(2000,3,14), Date.new(2007,3,15), [7, 7, 7])
tryit2(Date.new(2000,3,15), Date.new(2007,3,15), [7, 7, 7])
tryit2(Date.new(2000,3,16), Date.new(2007,3,15), [6, 6, 6])
tryit2(Date.new(2000,2,29), Date.new(2007,2,27), [6, 6, 6])
tryit2(Date.new(2000,2,29), Date.new(2007,2,28), [6, 6, 7])
tryit2(Date.new(2000,2,29), Date.new(2007,3,1), [7, 7, 7])
tryit2(Date.new(2000,2,29), Date.new(2007,3,2), [7, 7, 7])
tryit2(Date.new(2004,3,1), Date.new(2007,3,1), [3, 3, 3])

rick@frodo:/public/rubyscripts$ ruby datemath.rb
There are 7 years between 2000-03-14 and 2007-03-15
With leapling_date of 2000-03-01
There are 7 years between 2000-03-14 and 2007-03-15
With leapling_date of 2000-02-28
There are 7 years between 2000-03-14 and 2007-03-15

There are -7 years between 2007-03-15 and 2000-03-14
With leapling_date of 2000-03-01
There are -7 years between 2007-03-15 and 2000-03-14
With leapling_date of 2000-02-28
There are -7 years between 2007-03-15 and 2000-03-14

There are 7 years between 2000-03-15 and 2007-03-15
With leapling_date of 2000-03-01
There are 7 years between 2000-03-15 and 2007-03-15
With leapling_date of 2000-02-28
There are 7 years between 2000-03-15 and 2007-03-15

There are -7 years between 2007-03-15 and 2000-03-15
With leapling_date of 2000-03-01
There are -7 years between 2007-03-15 and 2000-03-15
With leapling_date of 2000-02-28
There are -7 years between 2007-03-15 and 2000-03-15

There are 6 years between 2000-03-16 and 2007-03-15
With leapling_date of 2000-03-01
There are 6 years between 2000-03-16 and 2007-03-15
With leapling_date of 2000-02-28
There are 6 years between 2000-03-16 and 2007-03-15

There are -6 years between 2007-03-15 and 2000-03-16
With leapling_date of 2000-03-01
There are -6 years between 2007-03-15 and 2000-03-16
With leapling_date of 2000-02-28
There are -6 years between 2007-03-15 and 2000-03-16

There are 6 years between 2000-02-29 and 2007-02-27
With leapling_date of 2000-03-01
There are 6 years between 2000-02-29 and 2007-02-27
With leapling_date of 2000-02-28
There are 6 years between 2000-02-29 and 2007-02-27

There are -6 years between 2007-02-27 and 2000-02-29
With leapling_date of 2000-03-01
There are -6 years between 2007-02-27 and 2000-02-29
With leapling_date of 2000-02-28
There are -6 years between 2007-02-27 and 2000-02-29

There are 6 years between 2000-02-29 and 2007-02-28
With leapling_date of 2000-03-01
There are 6 years between 2000-02-29 and 2007-02-28
With leapling_date of 2000-02-28
There are 7 years between 2000-02-29 and 2007-02-28

There are -6 years between 2007-02-28 and 2000-02-29
With leapling_date of 2000-03-01
There are -6 years between 2007-02-28 and 2000-02-29
With leapling_date of 2000-02-28
There are -7 years between 2007-02-28 and 2000-02-29

There are 7 years between 2000-02-29 and 2007-03-01
With leapling_date of 2000-03-01
There are 7 years between 2000-02-29 and 2007-03-01
With leapling_date of 2000-02-28
There are 7 years between 2000-02-29 and 2007-03-01

There are -7 years between 2007-03-01 and 2000-02-29
With leapling_date of 2000-03-01
There are -7 years between 2007-03-01 and 2000-02-29
With leapling_date of 2000-02-28
There are -7 years between 2007-03-01 and 2000-02-29

There are 7 years between 2000-02-29 and 2007-03-02
With leapling_date of 2000-03-01
There are 7 years between 2000-02-29 and 2007-03-02
With leapling_date of 2000-02-28
There are 7 years between 2000-02-29 and 2007-03-02

There are -7 years between 2007-03-02 and 2000-02-29
With leapling_date of 2000-03-01
There are -7 years between 2007-03-02 and 2000-02-29
With leapling_date of 2000-02-28
There are -7 years between 2007-03-02 and 2000-02-29

There are 3 years between 2004-03-01 and 2007-03-01
With leapling_date of 2000-03-01
There are 3 years between 2004-03-01 and 2007-03-01
With leapling_date of 2000-02-28
There are 3 years between 2004-03-01 and 2007-03-01

There are -3 years between 2007-03-01 and 2004-03-01
With leapling_date of 2000-03-01
There are -3 years between 2007-03-01 and 2004-03-01
With leapling_date of 2000-02-28
There are -3 years between 2007-03-01 and 2004-03-01

--
Rick DeNatale

My blog on Ruby
http://talklikeaduck.denhaven2.com/

Depending on what you mean with this, I believe Visual Basic (gasp !
shock ! horror !) has a DateSerial function that can overflow in any
direction. If you want to know the last day of february this year, you
can write DateSerial(2007, 3, 0), for instance.

It's extremely useful. (And you have the same with TimeSerial, even if
it's a bit less useful.)

Fred

···

Le 16 mars à 11:49, Colin Bartlett a écrit :

Extending the original poster's question, as people here are aware
working in complete months in Gregorian calendars can be tricky. For
example, if you are trying to generate a series of dates at intervals
of an integer number of months (years are just 12 months) then - as
far as I'm aware - there's no "standard" way of doing it in Ruby, or
in many other computer languages for that matter. (I'd be delighted to
be contradicted: this also applies to anything else I've got wrong in

--
When I was a child I had a fever. My hands felt just like two balloons.
                   Now I got that feeling once again.
I can't explain, you would not understand. This is not how I am.
I have become comfortably numb. (Pink Floyd, Comfortably Numb)

I remember reading an article about why it can be important though.

For example, in a program which manages a doctor's surgery, it has to be
able to calculate vaccination schedules. These are often specified in
months, e.g. "second vaccination must be 4 to 6 months after the first
vaccination". Unfortunately this means that the minimum and maximum number
of days between vaccinations varies depending on exactly when in the year
you had the first one.

Now of course this is completely ludicrous - the body clock doesn't run in
months, and the medical people should have specified the intervals in days
or weeks. But database programmers are not qualified to make such medical
judgements. And therefore, they have to implement the rules as laid down by
the medical authority.

Regards,

Brian.

···

On Fri, Mar 16, 2007 at 07:49:16PM +0900, Colin Bartlett wrote:

Extending the original poster's question, as people here are aware
working in complete months in Gregorian calendars can be tricky.