In Perl a function called timelocal exists to convert times to epoch.
example: my $epoch = timelocal($sec, $min, $hour, $mday, $mon, $year)
This function however appears to be inherently flawed when dealing with times very far in the past (before the year 999) - see the section on Year Value Interpretation. To make things worse, the way it processes 2 digit years complicates things even more...
Given a time before the year 999 how can I accurately convert it to its corresponding epoch value?
Given a time before the year 999 how can I accurately convert it to its corresponding epoch value?
You can't with Time::Local. timegm (which is used by timelocal) contains the following:
if ( $year >= 1000 ) {
$year -= 1900;
}
elsif ( $year < 100 and $year >= 0 ) {
$year += ( $year > $Breakpoint ) ? $Century : $NextCentury;
}
If the year is between 0 and 100, it's automatically converted to a year in the current century as described in the documentation; years between 100 and 999 are treated as offsets from 1900. You can't get around that without hacking the source.
If your perl was compiled to use 64-bit integers*, you can use the DateTime module instead:
use strict;
use warnings 'all';
use 5.010;
use DateTime;
my $dt = DateTime->new(
year => 1,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
time_zone => 'UTC'
);
say $dt->epoch;
Output:
-62135596800
Note that the Gregorian calendar wasn't even adopted until 1582, so DateTime uses what's called the "proleptic Gregorian calendar" by simply extending it backwards from 1582.
* With 32-bit integers, dates too far in the past or future will cause integer overflow. Your perl supports 64-bit integers if use64bitint=define appears in the output to perl -V (with a capital 'V').
Looking at the votes and reviews, the DateTime module would seem to be the authoritative, go-to module for this sort of stuff. Unfortunately its $dt->epoch() documentation comes with these caveats;
Since the epoch does not account for leap seconds, the epoch time for
1972-12-31T23:59:60 (UTC) is exactly the same as that for 1973-01-01T00:00:00.
This module uses Time::Local to calculate the epoch, which may or may not
handle epochs before 1904 or after 2038 (depending on the size of your system's
integers, and whether or not Perl was compiled with 64-bit int support).
It would appear these are the limits you going to have to work within.
Having said that, this comment is probably a sensible warning for users who
Are using a machine with 32-bit ints; or
Have a low error tolerance even for "old" dates
The first is going to be a problem if you have a 32-bit machine. The range (in years) for a signed 32-bit based epoch is around 2^31 / (3600*24*365) or (only) 68 years to/from 1970 (presuming a unix epoch). For a 64 bit int however, it becomes 290,000 years to/from 1970 - which would be ok, I presume. :-)
Only you can say if the second issue is going to be a problem.
Herewith are the results of a back-of-the-envelope examination of the degree of error;
$ perl -MDateTime -E 'say DateTime->new( year => 0 )->epoch / (365.25 * 24 * 3600)'
-1969.96030116359 # Year 0ad is 1969.96 years before 1970
$ perl -MDateTime -E 'say DateTime->new( year => -1000 )->epoch / (365.25*24*3600)'
-2969.93839835729 # year 1000bc is 2969.94 years before 1970
$ perl -MDateTime -E 'say ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (365.25*24*3600))'
-999.978097193703 # 1,000bc has an error of 0.022 years
$ perl -MDateTime -E 'say 1000*365.25 + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
8 # ... or 8 days
$
NOTE: I don't know how much of this "error" is due to the way I'm examining it - a year is not 365.25 days. In fact, let me correct that - I took a better definition of days in a year from here and we get;
$ perl -MDateTime -E 'say 1000*365.242189 + ((DateTime->new( year => -1000 )->epoch - DateTime->new( year => 0 )->epoch ) / (24 * 3600))'
0.189000000013039
So, an error of something-like 0.2 days when working with dates around 1,000bc.
In short, if you have 64 bit machine, you should be fine.
The Gregorian calendar repeat completely every 146,097 days, which equals 400 years. We can map a year less than 1000 to an equivalent year within the cycle. The following implementation map a year less than 1000 to a year in the third cycle, for example 0001 maps to 1201.
#!/usr/bin/perl
use strict;
use warnings;
use Time::Local qw[timegm timelocal];
use constant CYCLE_YEARS => 400;
use constant CYCLE_DAYS => 146097;
use constant CYCLE_SECONDS => CYCLE_DAYS * 86400;
sub mytimelocal {
my ($sec, $min, $hour, $mday, $month, $year) = #_;
my $adjust = 0;
if ($year < 1000) {
my $cycles = 3 - int($year/CYCLE_YEARS) + ($year < 0);
$year += $cycles * CYCLE_YEARS;
$adjust = $cycles * CYCLE_SECONDS;
}
return timelocal($sec, $min, $hour, $mday, $month, $year) - $adjust;
}
use Test::More tests => CYCLE_DAYS;
use constant STD_OFFSET =>
timelocal(0, 0, 0, 1, 0, 1200) - timegm(0, 0, 0, 1, 0, 1200);
my $seconds = -5 * CYCLE_SECONDS; # -0030-01-01
while ($seconds < -4 * CYCLE_SECONDS) { # 0370-01-01
my #tm = gmtime($seconds);
$tm[5] += 1900;
my $got = mytimelocal(#tm);
my $exp = $seconds + STD_OFFSET;
is($got, $exp, scalar gmtime($seconds));
$seconds += 86400;
}
Related
I'm having troubles with the difference calculation between two dates.
my $todayDate = DateTime->now;
my #updateDateFields = split /\//, $proteinObj->{lastUpdate}; #yyyy/mm/dd
my $updateDateTime = DateTime->new(
year => #updateDateFields[0],
month=> #updateDateFields[1],
day=> #updateDateFields[2]
);
my $daysSinceLastUpdate = $todayDate - $updateDateTime;
my $dfd = DateTime::Format::Duration->new(pattern => '%Y years, %m months, %e days');
print "Last update was: ". $dfd->format_duration($daysSinceLastUpdate). " ago.\n";
And the output is this:
Last update date: 2015/01/13 Last update was: 0 years, 22 months, 0
days ago.
It does't display 1 years, 10 months, 0 days ago.
You need to enable the normalise option in the DateTime::Format::Duration object, like this
my $dfd = DateTime::Format::Duration->new(
pattern => '%Y years, %m months, %e days',
normalise => 1,
);
In perl, given a user inputted date, how can I check that is not greater 12 months from today?
I tried this way:
#!/usr/bin/env perl
use 5.010;
use warnings;
use DateTime;
use Data::Dumper;
$given = DateTime->new( year=>"2013", month => "11", day =>"23" );
$now = DateTime->now;
$delta = $given->delta_md($now);
say $delta->months;
print Dumper($delta);
But the output I got was this. Why $delta->months value is different than that showed from dumper?
11
$VAR1 = bless( {
'seconds' => 0,
'minutes' => 0,
'end_of_month' => 'wrap',
'nanoseconds' => 0,
'days' => 24,
'months' => 23
}, 'DateTime::Duration' );
The method months in DateTime::Duration is the remainder month part of the duration after conversion to the larger unit (year). The internal data structure stores the complete duration (1a, 11m) in a different way.
years, months, weeks, days, hours, minutes, seconds, nanoseconds
These methods return numbers indicating how many of the given unit the object represents,
after having done a conversion to any larger units. For example, days are first converted
to weeks, and then the remainder is returned. These numbers are always positive.
To get this value I think you need $dur->in_units( 'months' );.
$delta->months is always < 12. Check that $delta->years > 1.
my $limit = DateTime->new( year=>"2013", month => "11", day =>"23" );
my $today = DateTime->today();
if ($now->clone()->add( months => 12 ) > $limit) {
...
}
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.
Have you tried DateTime? It'll handle parsing as well as subtraction.
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.
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
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 )
I want to calculate (using the default Perl installation only) the number of days between two dates. The format of both the dates are like so 04-MAY-09. (DD-MMM-YY)
I couldn't find any tutorials that discussed that date format. Should I be building a custom date checker for this format? Further reading of the Date::Calc on CPAN it looks unlikely that this format is supported.
There seems to be quite a bit of confusion because, depending on what you are trying to accomplish, “the number of days between two dates” can mean at least two different things:
The calendar distance between the two dates.
The absolute distance between the two dates.
As an example and to note the difference, assume that you have two DateTime objects constructed as follows:
use DateTime;
sub iso8601_date {
die unless $_[0] =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/;
return DateTime->new(year => $1, month => $2, day => $3,
hour => $4, minute => $5, second => $6, time_zone => 'UTC');
}
my $dt1 = iso8601_date('2014-11-04T23:35:42Z');
my $dt2 = iso8601_date('2014-11-07T01:15:18Z');
Note that $dt1 is quite late on a Tuesday, while $dt2 is very early on the following Friday.
If you want the calendar distance use:
my $days = $dt2->delta_days($dt1)->delta_days();
print "$days\n" # -> 3
Indeed, between, Tuesday and Friday there are 3 days. A calendar distance of 1 means “tomorrow” and a distance of -1 means “yesterday”. The “time” part of the DateTime objects is mostly irrelevant (except perhaps if the two dates fall on different time zones, then you would have to decide what “the calendar distance” between those two dates should mean).
If you want the absolute distance then instead use:
my $days = $dt2->subtract_datetime_absolute($dt1)->delta_seconds / (24*60*60);
print "$days\n"; # -> 2.06916666666667
Indeed, if you want to split the time between the two dates in 24-hour chunks, there are only about 2.07 days between them. Depending on your application, you might want to truncate or round this number. The “time” part of the DateTime objects is very relevant, and the expected result is well defined even for dates on different time zones.
If you care about accuracy, keep in mind that not all days have 86400 seconds. Any solution based on that assumption will not be correct for some cases.
Here's a snippet I keep around to calculate and display date/time differences a few different ways using the DateTime library. The last answer printed is the one you want, I think.
#!/usr/bin/perl -w
use strict;
use DateTime;
use DateTime::Format::Duration;
# XXX: Create your two dates here
my $d1 = DateTime->new(...);
my $d2 = DateTime->new(...);
my $dur = ($d1 > $d2 ? ($d1->subtract_datetime_absolute($d2)) :
($d2->subtract_datetime_absolute($d1)));
my $f = DateTime::Format::Duration->new(pattern =>
'%Y years, %m months, %e days, %H hours, %M minutes, %S seconds');
print $f->format_duration($dur), "\n";
$dur = $d1->delta_md($d2);
my $dy = int($dur->delta_months / 12);
my $dm = $dur->delta_months % 12;
print "$dy years $dm months ", $dur->delta_days, " days\n";
print $dur->delta_months, " months ", $dur->delta_days, " days\n";
print $d1->delta_days($d2)->delta_days, " days\n";
Time::ParseDate will handle that format just fine:
use Time::ParseDate qw(parsedate);
$d1="04-MAR-09";
$d2="06-MAR-09";
printf "%d days difference\n", (parsedate($d2) - parsedate($d1)) / (60 * 60 * 24);
Date::Calc has Decode_Date_EU (and US etc)
#!/usr/bin/perl
use Date::Calc qw(Delta_Days Decode_Date_EU);
($year1,$month1,$day1) = Decode_Date_EU('02-MAY-09');
($year2,$month2,$day2) = Decode_Date_EU('04-MAY-09');
print "Diff = " . Delta_Days($year1,$month1,$day1, $year2,$month2,$day2);
This question already has a nice answer, but I want to provide a answer showing why calculating the difference in seconds is WRONG (when we're using formatted/local dates rather than floating dates).
I find it distressing how many suggestions tell people to subtract seconds. (This question was the first Google hit for my search, so I don't care how old it is.)
I've made that mistake myself and wondered why the application would suddenly (over the weekend) show incorrent times. So I'm hoping this code will help people (who may be facing such an issue) understand why this approach is wrong and help them avoid that mistake.
Here is a complete example, one that doesn't contain "..." at some crucial point (because if you insert two dates in the same time zone, you may not see an error).
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
use DateTime;
# Friday, Oct 31
my $dt1 = DateTime->new(
time_zone => "America/Chicago",
year => 2014,
month => 10,
day => 31,
);
my $date1 = $dt1->strftime("%Y-%m-%d (%Z %z)");
# Monday, Nov 01
my $dt2 = $dt1->clone->set(month => 11, day => 3);
my $date2 = $dt2->strftime("%Y-%m-%d (%Z %z)");
# Friday, Mar 06
my $dt3 = DateTime->new(
time_zone => "America/Chicago",
year => 2015,
month => 3,
day => 6,
);
my $date3 = $dt3->strftime("%Y-%m-%d (%Z %z)");
# Monday, Mar 09
my $dt4 = $dt3->clone->set(day => 9);
my $date4 = $dt4->strftime("%Y-%m-%d (%Z %z)");
# CDT -> CST
print "dt1:\t$dt1 ($date1):\t".$dt1->epoch."\n";
print "dt2:\t$dt2 ($date2):\t".$dt2->epoch."\n";
my $diff1_duration = $dt2->subtract_datetime_absolute($dt1);
my $diff1_seconds = $diff1_duration->seconds;
my $diff1_seconds_days = $diff1_seconds / 86400;
print "diff:\t$diff1_seconds seconds = $diff1_seconds_days days (WRONG)\n";
my $diff1_seconds_days_int = int($diff1_seconds_days);
print "int:\t$diff1_seconds_days_int days (RIGHT in this case)\n";
print "days\t".$dt2->delta_days($dt1)->days." days (RIGHT)\n";
print "\n";
# CST -> CDT
print "dt3:\t$dt3 ($date3):\t".$dt3->epoch."\n";
print "dt4:\t$dt4 ($date4):\t".$dt4->epoch."\n";
my $diff3_duration = $dt4->subtract_datetime_absolute($dt3);
my $diff3_seconds = $diff3_duration->seconds;
my $diff3_seconds_days = $diff3_seconds / 86400;
print "diff:\t$diff3_seconds seconds = $diff3_seconds_days days (WRONG)\n";
my $diff3_seconds_days_int = int($diff3_seconds_days);
print "int:\t$diff3_seconds_days_int days (WRONG!!)\n";
print "days\t".$dt4->delta_days($dt3)->days." days (RIGHT)\n";
print "\n";
Output:
dt1: 2014-10-31T00:00:00 (2014-10-31 (CDT -0500)): 1414731600
dt2: 2014-11-03T00:00:00 (2014-11-03 (CST -0600)): 1414994400
diff: 262800 seconds = 3.04166666666667 days (WRONG)
int: 3 days (RIGHT in this case)
days 3 days (RIGHT)
dt3: 2015-03-06T00:00:00 (2015-03-06 (CST -0600)): 1425621600
dt4: 2015-03-09T00:00:00 (2015-03-09 (CDT -0500)): 1425877200
diff: 255600 seconds = 2.95833333333333 days (WRONG)
int: 2 days (WRONG!!)
days 3 days (RIGHT)
Notes:
Again, I'm using local dates. If you use floating dates, you won't have that problem - simply because your dates stay in the same time zone.
Both time ranges in my example go from friday to monday, so the difference in days is 3, not 3.04... and of course not 2.95...
Turning the float into an integer using int() (as suggested in an answer) is just wrong, as shown in the example.
I do realize that rounding the difference in seconds would also return correct results in my example, but I feel like it's still wrong. You'd be calculating a day difference of 2 (for a large value of 2) and, because it is a large value of 2, turn it into a 3. So as long as DateTime provides the functionality, use DateTime.
Quoting the documentation (delta_days() vs subtract_datetime()):
date vs datetime math
If you only care about the date (calendar) portion of a datetime, you
should use either delta_md() or delta_days(), not subtract_datetime().
This will give predictable, unsurprising results, free from
DST-related complications.
Bottom line: Don't diff seconds if you're using DateTime. If you're not sure what date framework to use, use DateTime, it's awesome.
You could convert the dates into the long integer format, which is the number of seconds since the epoch (some date in 1970 I think). You then have two variables that are the dates in seconds; subtract the smaller from the larger. Now you have a time span in seconds; divide it by the number of seconds in 24 hours.
Convert the two dates to seconds and then do the math:
#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw/mktime/;
{
my %mon = (
JAN => 0,
FEB => 1,
MAR => 2,
APR => 3,
MAY => 4,
JUN => 5,
JUL => 6,
AUG => 7,
SEP => 8,
OCT => 9,
NOV => 10,
DEC => 11,
);
sub date_to_seconds {
my $date = shift;
my ($day, $month, $year) = split /-/, $date;
$month = $mon{$month};
if ($year < 50) { #or whatever your cutoff is
$year += 100; #make it 20??
}
#return midnight on the day in question in
#seconds since the epoch
return mktime 0, 0, 0, $day, $month, $year;
}
}
my $d1 = "04-MAY-99";
my $d2 = "04-MAY-00";
my $s1 = date_to_seconds $d1;
my $s2 = date_to_seconds $d2;
my $days = int(($s2 - $s1)/(24*60*60));
print "there are $days days between $d1 and $d2\n";
How do I find a date which is 3 days earlier than a given date in Perl where the format is YYYY-MM-DD?
Date::Calc is the champion module here:
use strict;
use warnings;
use Date::Calc qw(Add_Delta_YMD);
my $startDate = '2000-01-01';
my ($startYear, $startMonth, $startDay) = $startDate =~ m/(\d{4}-(\d{2})-\d{2})/;
# 1 year, 2 months, 3 days, after startDate
my $endDate = join('-', Add_Delta_YMD($startYear, $startMonth, $startDay, 1, 2, 3));
The module has a huge number of time conversion routines, particularly those dealing with deltas. DateTime and Date::Manip are also worth checking out.
Date::Calc can be used for such calculations:
#!/usr/bin/perl
use strict;
use warnings;
use Date::Calc qw(Add_Delta_Days);
my ( $yyyy, $mm, $dd ) = ( 2009, 9, 2 );
my #date = Add_Delta_Days( $yyyy, $mm, $dd, -3 );
print join( '-', #date );
DateTime is the canonical way for dealing with dates in modern Perl:
use DateTime;
my ($year, $month, $day) = split '-', '2009-09-01';
my $date = DateTime->new( year => $year, month => $month, day => $day );
$date->subtract( days => 3 );
# $date is now three days earlier (2009-08-29T00:00:00)
There are so many options that it is moderately embarrassing. It depends in part on what other calculations you might need in the future, and whether times and time zones will ever be a factor, and similar things.
You could look at any of these
Date::Calc
Date::Manip
DateTime::* - see also datetime.perl.org (though that site did not seem to be responding on 2009-09-01T22:30-07:00)
to name but three (sets of) modules. I'd suggest Date::Calc or Date::Manip for simplicity - but if you're going to need to get fancy in future, the DateTime modules may be better.
See perldoc POSIX for the function mktime().
It will help you convert dates and times to a simple number, which is the Unix time (the number of seconds since January 1, 1970, I believe). Then, just subtract 3 (days) times 24 (hours in a day) times 60 (minutes in an hour) times 60 (seconds in a minute), or 259200 seconds from that number, and use localtime() to convert that number of seconds back to a string representation.
This is probably the best solution*, because it will handle month and year changes automatically. Any other solution will probably end up being more complicated after factoring in checking to see if we ran out of days in a month, or ran out of months in a year.
EDIT: *Outside of looking on CPAN.
The neat thing about mktime is that it will handle any time of offset. It uses January=0; and Year 2009 = 109 in this scheme. Thus, printed month - 1 and full year - 1900.
use POSIX qw<mktime>;
my ( $year, $month, $day ) = split '-', $date;
my $three_day_prior = mktime( 0, 0, 0, $day - 3, $month - 1, $year - 1900 );
mktime is useful for finding the last day of the month as well. You just go to day 0 of the next month.
mktime( 0, 0, 0, 0, $month, $year - 1900 );
This is simple with Date::Simple
C:\>perl -MDate::Simple=today -e "print today()-3"
2009-08-30