Calculate Age, days to next and days from last for a given birthday
#!/usr/bin/perl
use strict;
use warnings;
use Time::Local qw(timelocal_nocheck);
# calculate Age, days from last birthday and days to next birthday for
# a given birth date.
# birthdate is a string of the format 'YYYY-MM-DD'
# - good for accepting dates from databases
# If the birthdate is today, then days from = days to = 0
# returns age, days from, days to
# NOTE: this script does no date validation
# NOTE: this script uses the current year to calculate age.
# NOTE: if you modify this script to use anything other than current year,
# there may be leap year implications
sub birth_date_age_today {
my $birthdate = shift || return 0;
$birthdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)$/ or return 0;
my ($byyyy,$bmm,$bdd) = ($1,$2,$3);
--$bmm;
my ($yyyy) = (localtime)[5];
$yyyy += 1900;
my $cur_day_of_year = (localtime)[7];
# nocheck or feb 29 birthdays will have problems
my $birth_day_of_year = (localtime timelocal_nocheck(0, 0, 0, $bdd, $bmm, $yyyy - 1900))[7];
# calculate age
my $age = $yyyy - $byyyy;
my $daysto;
my $daysfrom;
if ($cur_day_of_year < $birth_day_of_year) {
# haven't hit birthday yet this year
$age--;
$daysto = $birth_day_of_year - $cur_day_of_year;
# last year's birthday
$yyyy--;
$birth_day_of_year = (localtime timelocal_nocheck(0, 0, 0, $bdd, $bmm, $yyyy - 1900))[7];
# correct for days (2100 isn't a leap year)
$daysfrom = (($yyyy % 4 or $yyyy == 2100) ? 365 : 366) - $birth_day_of_year + $cur_day_of_year;
}
elsif ($cur_day_of_year > $birth_day_of_year) {
# passed birthday this year
$daysfrom = $cur_day_of_year - $birth_day_of_year;
# next year's birthday
# Note: we get the number of days to the birthday in the next year ($yyyy - 1899)
# but we use the current year for checking leap year because we need to know how
# many days are left in this year
$birth_day_of_year = (localtime timelocal_nocheck(0, 0, 0, $bdd, $bmm, $yyyy - 1899))[7];
# correct for leap days (2100 isn't a leap year)
$daysto = (($yyyy % 4 or $yyyy == 2100) ? 365 : 366) - $cur_day_of_year + $birth_day_of_year;
}
else { $daysfrom = $daysto = 0 }
return $age, $daysfrom, $daysto;
}
# a simple test script
# run through every date (birthdate) of a given year
# print the results (relative to current date)
my %days = (
1 => 31,
2 => 28,
3 => 31,
4 => 30,
5 => 31,
6 => 30,
7 => 31,
8 => 31,
9 => 30,
10 => 31,
11 => 30,
12 => 31,
);
my $yyyy = 1965;
# This is not totally correct! Just simplified for this test
$days{2} = ($yyyy % 4 or $yyyy == 1900 or $yyyy == 2100) ? 28 : 29;
for (my $mm = 1; $mm <= 12; ++$mm) {
for (my $dd = 1; $dd <= $days{$mm}; ++$dd) {
my $testdate = sprintf("$yyyy-%02d-%02d", $mm, $dd);
my ($age, $daysfrom, $daysto) = birth_date_age_today($testdate);
print "birthdate $testdate age $age from last $daysfrom days to $daysto\n";
}
}
Re: Calculate Age, days to next and days from last for a given birthday
For dates (i.e. times with h,m,s=0), it's better to use timegm+gmtime over timelocal+localtime. I think your program will return an incorrect answer for some time periods due to DST.
Update:
Solution which still uses core module [mod://Time::Local]:
use strict;
use warnings;
use Time::Local qw( timegm_nocheck );
# birth_date_age(2006, 4, 28) for people born on April 28th, 2006.
sub birth_date_age {
my ($bday_y, $bday_m, $bday_d) = @_;
# ----------------------------------------
# -- Get current date.
my ($now_y, $now_m, $now_d) = (localtime)[5,4,3];
$now_y += 1900;
$now_m++;
my $date_now = timegm_nocheck(0,0,0, $now_d, $now_m-1, $now_y);
# ----------------------------------------
# -- Get next birthday.
my $date_next = timegm_nocheck(0,0,0, $bday_d, $bday_m-1, $now_y);
$date_next = timegm_nocheck(0,0,0, $bday_d, $bday_m-1, $now_y+1)
if $date_next <= $date_now;
my ($next_y) = (gmtime($date_next))[5];
$next_y += 1900;
# ----------------------------------------
# -- Get prev birthday.
my $prev_y = $next_y - 1;
my $date_prev = timegm_nocheck(0,0,0, $bday_d, $bday_m-1, $prev_y);
# ----------------------------------------
# -- Calculate age and others
my $age = $next_y - $bday_y - 1;
my $days_to = ($date_next - $date_now) / (24*60*60);
my $days_from = ($date_now - $date_prev) / (24*60*60);
return ($age, $days_from, $days_to);
}
Notes:
-
On years without a Feb 29th, March 1st will be considered the birthday anniversary of people born on Feb 29th.
-
I leave the date parsing to the caller. It makes for a more flexible solution.
-
For arguments, days, months and years are one-based.
-
Internally, days, months and years are one-based.
Re^2: Calculate Age, days to next and days from last for a given birthday
The time period in this case would I guess be midnight as that's when the date would switch over. Wouldn't it be more appropriate to have that happen on local time? Otherwise your birthdate wouldn't flip until gmtime midnight, which wouldn't be correct (unless you happen to be living on the gm time line).
Re^3: Calculate Age, days to next and days from last for a given birthday
As you can see in my (newly added) code, I work with *local* dates using timegm+gmtime. The number of days bewteen two dates is the same no matter which time zone you are in.
Re^4: Calculate Age, days to next and days from last for a given birthday
OK, I see now how your code works works (took me a while, sorry). But I'm still confused as to the need to convert to seconds.
my $days = (localtime)[7];
$days will be correct for the current date regardless of the time or DST no? All other days are calculated as of 0:00:00 which should also be correct for any given date regardless of DST. Or am I missing something about the way localtime/timelocal work?
(living in a time zone with no DST does have it's disadvantages here).
I never thought of feb 29 birthdates failing as invalid dates in non-leap years!
Re^5: Calculate Age, days to next and days from last for a given birthday
But I'm still confused as to the need to convert to seconds.
You can't do arithmetic on years+months+days. Just like you can't do arithmetic on degrees+minutes+seconds. You need to convert it into a number.
I could have converted every date into the number of days since an arbitrary day, but there's no existing function to do that (or the inverse operation). Doing it manually would force me to handle leap seconds, leap days, oddly numbered months, etc.
On the other hand, there's already a function (timegm) to convert a date into the number of seconds since an arbitrary second (Jan 1st, 1970, 0:00:00), and there's already a function which does the inverse operation (gmtime).
Re^6: Calculate Age, days to next and days from last for a given birthday
Isn't converting years+months+days into a number exactly what (localtime)[7] does? It's returning the day number (on a scale of 1 to 365 or 366) of the current date in the current year. The conversion has been done, leap seconds, DST, odd months, leap years and all.
I'm not trying to calculate the number of days since birth, or even the number of days between two dates for that matter, just the day number of the birth date in an arbitrary year (either last or next). While it may look like the code is trying to calculate based on the number of days from a common reference, it's actually calculating based on the day number of given year (with an adjustment for an extra leap day where neccessary).
For example Jan 1 this year is day 1. Today is day 117. If the birthdate in question was Jan 1, the days from birthday is simply 117 - 1 = 116. Jan 1 next year is also day 1. This year has 365 days, so days to next birthday is 365 - 117 + 1 = 249 (days in the year - days from today to the end of this year + days to date in next year). At no time am I actually trying to calculate the number of days between two dates, only the number of days since the beginning of a year, which (localtime)[7] does nicely. It even works through leap months, as long as you're careful about the what you consider is the number of days in the year.
Or am I still missing it?
Re^7: Calculate Age, days to next and days from last for a given birthday
Isn't converting years+months+days into a number exactly what (localtime)[7] does?
No, it only combines month and day, not year, month and day. Without taking the year into account, you need to work around leap years. I didn't have to. You have to, and you did, but incorrectly. (Years divisible by 4 are leap years, except those divisible by 100, except those divisible by 400.)
Actually, it just occured to me that both of our solutions have the following problems: They can't handle dates < Jan 1st 1970 or >= (some point in) 2038. Not very useful.
Re^8: Calculate Age, days to next and days from last for a given birthday
Again with the leap years...
As I explained to davidrw, the script as written (both mine and your version) uses current year +/- 1 year, not some arbitrary year in the past or future. At no time is any other year passed into a date function.
Which means it breaks if you can go back in time to run it before 1970 (which, if you've found a way to do it, please let me know cause there are some investments I'd like to make). It also breaks if you're still using it by 2038, but that's a Perl internal date problem and well beyond the control of this script (and every other Perl script written that uses current date).
So neither of our scripts will ever have to deal with dates < Jan 1st 1970 or >= (some point in) 2038. Not as written anyway
Re^9: Calculate Age, days to next and days from last for a given birthday
Not again, still. You asked what was wrong with
(localtime)[7], and I told you. You need to handle leap years (contrary to what you said). You attempted to do so, but it's buggy. It's wrong for 1900 and 2100, for starters. You might say that's not a problem because the system doesn't accept those dates, but it's a bug that it can't accept those dates. Once you fix your solution to accepts those dates, you'll have to fix the problem with the year 1900 and 2100. So that's two bugs. You're also off by a day (I think) when you run the program with certain combinations of current time, current date and current timezones for a total of three bugs.
Re^10: Calculate Age, days to next and days from last for a given birthday
???
But I do handle leap years? Just not the year 2100.
1900 is in the past. It has nothing to do with my script, how it's written or whether it's a leap year. 1900 is the past, is the past, is the past. The only way you can reach it is to roll back you system clock so that your system thinks the current year is 1900 which is far beyond the use or intentions of this script. Do we modify all our scripts to accept the possibility of someone resetting the system clock to some abnormal date?
It's not a bug that it can't accept those dates? That's not what the script does. There's no argument to accept an arbitrary second date to use. You're suggesting it's a bug that the script doesn't accept something it doesn't use.
If it's necessary to appease the gods to account for the possibility of this script being used in the year 2100 I'll do it, as unrational as I think that is. To account for 1900 is just plain wrong IMHO.
I'd like to see an example of off by a day with certain combinations of current time, current date and current timezones.
Re^11: Calculate Age, days to next and days from last for a given birthday
I forgot that the birthday year doesn't matter, just the current year +- 1 matters. I was thinking more generally when you asked about
(localtime)[7], not how it applied here. While the way your calculate leap years is wrong in a general sense, it is a correct simplification for the expect inputs. A comment about this wouldn't hurt. ++.
I'd like to see an example of off by a day with certain combinations of current time, current date and current timezones.
Sure, tell me on which date the time spring forwards or falls back this year Note that I only *think* your program will return an incorrect answer for some time periods due to DST, as I have said earlier. I'm not sure what the circumstances are that causes the problem, which is why I use timegm+gmtime to completely avoid the issue.
Re^12: Calculate Age, days to next and days from last for a given birthday
Updated and commented.
Fixed for the year 2100.
Fixed for feb 29 birthdays.
Also removed current day and month (which have nothing to do with the script) to avoid confusion.
(hope it still works!)
Man, that was a long road to the answer!
Re^12: Calculate Age, days to next and days from last for a given birthday
Hey ikegami
I've updated the script again (missing brackets around the leap year check). I've also added a test script to run through all dates in a year. Days from/to should progress smoothly up and down to 0 on the current date, which they do for me (but I have no DST change). If there's a DST issue, I would expect a jump in the sequence on time change day. But it may also depend on the time of day (current time) when you run the script?
Re: Calculate Age, days to next and days from last for a given birthday
Where doesn't $session come from? This code wont run as is, and i'm not bright enough to fix it ;)
Re^2: Calculate Age, days to next and days from last for a given birthday
My bad. I ripped the function from my code, which actually did more stuff.
I've replaced the error with a regular current date.
Re: Calculate Age, days to next and days from last for a given birthday
$daysfrom = ($yyyy % 4 ? 365 : 366) isn't sufficient for leap years .. there's another part of the rule -- e.g. Feb 1900 only had 28 days but Feb 2000 had 29. [google://leap year calculation|google results]
I see you were trying to avoid other modules, but just for an example (and cause it's a module i use alot), here's a partial [cpan://Date::Calc] solution:
my $birthday = '2000-01-02'; # yyyy-mm-dd
use Date::Calc qw/Delta_Days Today Add_Delta_YMD/;
my @date = split /-/, $birthday;
$date[0] = (Today())[0]; # set the year to current year
print Delta_Days( Add_Delta_YMD(@date,-1,0,0), @date ); # days since last one
print Delta_Days( @date, Add_Delta_YMD(@date,1,0,0) ); # days until next one
Update: added in the Add_Delta_YMD to account for birthdays on 2/29
Re^2: Calculate Age, days to next and days from last for a given birthday
But when calculating days to/from birthdate, we don't deal with 1900. We only ever work with the current year (plus or minus one). I don't recall what the rules are regarding the 'leap year skip', but I'm pretty sure it won't affect this code for a very long time.
Re^3: Calculate Age, days to next and days from last for a given birthday
We only ever work with the current year (plus or minus one).
Which by definition includes the Feb/Mar boundry, so leap year can affect it by a day...
I don't recall what the rules are regarding the 'leap year skip',
See the google link above (searched for 'leap year calculation'); Also another reason to use
Date::Calc,
Date::Time, etc modules.
but I'm pretty sure it won't affect this code for a very long time.
Yeah.. i'm pretty sure that's what people said about the year 2000, which came and went, as will 2038 .. as will 2100 (which has only 28 days in Feb) .. Sure, that's a little way off, but on prinicpal you should care about the accuracy of your code, also, what about this realistic (at least plausible) scenario: Someone sees your snippet here and adapts it to their code, which isn't limited to the current year (e.g. days to/from a certain future date) and 2100 might be a valid input ..
Re^4: Calculate Age, days to next and days from last for a given birthday
I thought the category 'fun stuff' was obvious....
I take great pride in my code (even if it's sometimes wrong).
I also spent the better part of 1997 thru 1999 fixing broken mainframe date code for a living. If you want to compare a 30 year old 2 digit year problem to a 100 year old leap year problem I think there's a baby/bath-water comparison that could be made. For all you know, by 2100 the leap year rule may be changed into another exception just to prevent computer leap year problems from popping up! (if we're even using the same calendar system by then)
The code I've written here specifically uses current date +/- 1 year. Without rewriting portions of it, it will never see 1900. You, me and everything we know and believe about computers will be long since dead and turned to dust before this code ever sees 2100. So the scenario of someone using this snippet untouched for either of those years is not even a probability. That's the beauty of the 2000 'exception'.
The code as written (pending some revisions) will happily work with any birthdate of any year. You can use any number that Perl will accept for the birthdate year, it isn't even passed to a date function of any kind. There is no provision to provide any date other than the current date. To make such a modification would require more careful attention to leap years as you've suggested. It would also not be the code I wrote.
The point of my writing it was to avoid using Date::Calc. If Date::Calc were a core module I might have considered it first but it's not. There are times when you can't just pick the module of your dreams and toss it into the script.