Calculating the age given DOB

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.

Le 16 mars à 11:49, Colin B. 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

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

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 ? : [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

On 3/16/07, Pit C. [email protected] wrote:

There are 3 years between 2001-03-02 and 2004-03-01
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 ) 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.

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/

John J. escreveu:

I think we can honestly assume that if you are born on leap day, you
become highly tolerant of systems that don’t even accept leap day
birthdays and probably write Feb 28 or Mar 1 consistently by habit.
In fact a lot of jurisdictions around the world would not allow the
Registrar have a person born in a February 29th…

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

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

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.