views:

100

answers:

5

I am trying to figure out a way to calculate the year of birth for records when given the age to two decimals at a given date - in Perl.

To illustrate this example consider these two records:

date, age at date
25 Nov 2005, 74.23
21 Jan 2007, 75.38

What I want to do is get the year of birth based on those records - it should be, in theory, consistent. The problem is that when I try to derive it by calculating the difference between the year in the date field minus the age, I run into rounding errors making the results look wrong while they are in fact correct.

I have tried using some "clever" combination of int() or sprintf() to round things up but to not avail. I have looked at Date::Calc but cant see something I can use.

p.s. As many dates are pre-1970, I cannot not unfortunately use UNIX epoch for this.

+6  A: 

Have you tried DateTime? It'll handle parsing as well as subtraction.

Oesor
Thanks for the tip - I had not tried it no but I will keep it in mind for future reference.
Spiros
+4  A: 

Perl's gmtime and localtime functions have no problem handling negative input and dates before 1970.

use Time::Local;
$time = timegm(0,0,0,25,11-1,2005-1900);          # 25 Nov 2005
$birthtime = $time - (365.25 * 86400) * 74.23;    # ~74.23 years
print scalar gmtime($birthtime);                  # ==> Wed Sep 2 11:49:12 1931

The actual birthdate could be different by a few days, since one one-hundredth of a year only gives you a resolution of 3-4 days.

mobrule
Thanks - this seems to work fine. I was using Date::Calc which seems to have a problem handling pre 1970 epoch dates I think. The resolution is not an issue as I will just be comparing years.
Spiros
Depending on your system, the `gmtime` family of functions *might* have trouble with years before 1902 or after 2038. If your perl is one of these and there's any chance you'll be dealing with centenarians, you might want to use `DateTime` for safety.
hobbs
A: 

If you don't want to use any built-in date classes and functions, the naive approach would be to subtract the truncated number of years from the year, ten worry about the fractional portion. If the starting month * 28 is greater than the fractional part (in days) then don't even bother. Likewise, if the starting month * 31 + the starting day is less than the fractional part, then you most definitely have wrapped around and should subtract one year. Otherwise you'll have to actually calculate.

You can precalculate the start day # for each month, add the day in the date to find the current day no, then compare to the fractional portion * 365. Leap years get a little funkier, but as 1/365 is no different than 1/366 in two decimal places, I don't think you even have to worry...

lc
Using "naive approach" usually lead to bugs.Instead use strong modules from the CPAN.
dolmen
@dolmen Is there really a problem with, when stuck, actually *thinking* about the problem and coming up with a solution? Of course, if you can, use built-in functions and it will certainly save time and hassle. But do note, I prefaced this with "If you don't want to use any built-in date classes and functions" (mostly because I thought that was what the question was asking in the first place)...
lc
+1  A: 

I'd second Oesor's recommendation (second time today), and reiterate mobrule's reminder that perl handles negative dates. So DateTime is preferable.

But I would like to illustrate that this can be done with POSIX::mktime:

my ( $year1, $mon1, $day1 ) = qw<1944 7 1>;
my ( $year2, $mon2, $day2 ) = qw<2006 5 4>;

my $time1 = POSIX::mktime( (0) x 3, $day1, $mon1 - 1, 72 );
my $time2 = POSIX::mktime( (0) x 3, $day2, $mon2 - 1, 72 );
my $years = $year2 - $year1 - ( $time2 < $time1 ? 1 : 0 );
# 61 years

The caveat is that perl's internal clock handles dates back to December 14th, 1902 (actually 13th, after noon and before 6 PM), before which mktime starts returning undef. So for 99% of the people alive today, this will probably do.

Pointless trivia: scalar localtime( 0x80000000 ) : 'Fri Dec 13 15:45:52 1901' <- that's the cutoff ( 0x80000000 being 2s-complement minimum integer )

Axeman
A: 

Use DateTime and DateTime::Duration.

When you substract a DateTime::Duration from a DateTime you get an other DateTime.

use strict;
use warnings;
use DateTime::Format::Strptime;
use DateTime::Duration;

my $fmt = DateTime::Format::Strptime->new(
    pattern => '%d %b %Y',
    locale  => 'en_US',
);

my $start = $fmt->parse_datetime($ARGV[0]);
my $age = DateTime::Duration->new(years => $ARGV[1]);

my $birth = $start - $age;
print $fmt->format_datetime($birth), "\n";

Here is an example on how to invoke it:

$ perl birth.pl "25 Nov 2005" 74.23
25 Sep 1931
$ perl birth.pl "21 Jan 2007" 75.38
21 Sep 1931
dolmen