How to find the difference between two dates in perl? - perl

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) {
...
}

Related

Converting times before the year 999 to epoch

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;
}

How to calculate the number of days between 2 unix timestamps in perl?

There has any simple ways how to calculate the number of days between two unix timestamps in perl,
Ex, $time1= 1366601846 and $time2 = 1366431011, so the output is 2 days.
Even the differences between two unix time-stamps only two second, it should be consider as 1 day difference.
Thank before..
In Europe/Paris, those two timestamps are
2013/04/20 06:10:11
2013/04/22 05:37:26
It's not clear what you want.
In Europe/Paris, the difference in times is one day, 23 hours and 15 minutes. Rounded up, that's 2 days. If this is what you want, you can use
use DateTime qw( );
my $dt1 = DateTime->from_epoch( epoch => 1366601846, time_zone => 'local' );
my $dt2 = DateTime->from_epoch( epoch => 1366431011, time_zone => 'local' );
my ($days, $minutes, $ns) = ($dt1 - $dt2)->in_units(qw( days minutes nanoseconds ));
++$days if $minutes || $ns;
print("$days\n");
In Europe/Paris, the difference in calendar dates in 2 days. If this is what you want, you can use
use DateTime qw( );
my $dt1 = DateTime->from_epoch( epoch => 1366601846, time_zone => 'local' );
my $dt2 = DateTime->from_epoch( epoch => 1366431011, time_zone => 'local' );
my $days = $dt1->delta_days($dt2)->in_units('days');
print("$days\n");
Dividing by 24*60*60 is not always going to work, whichever of the above you meant. In both case, the answer is dependent on the time zone you specify.
To get a floating point number of the difference in days between two unix (epoch) dates, try this:
$days = abs ($time1 - $time2) / 86400;
Unix timestamps (or epoch timestamps) are a measure of the number of seconds since 1/1/1970 12AM UTC, so the raw difference is a difference in seconds. With 86400 being the total seconds in a day (60min * 60sec * 24hr), the result above is a difference in days.
However, this alone will leave you will a long floating-point number which you will probably want to trim down with either:
ceil() or floor() from the Posix module, or simply use printf or sprintf to change the variable value itself:
printf("%.0f", $days);
$days = sprintf("%.0f", $days);

Find week number within range of dates in Perl

I have a range of dates -- say 2012-01-30 .. 2012-04-06 -- which denote a set of weeks that fall within these dates. These weeks are numbered 1 through 10. Given today's date (for instance, 2012-02-29), I want to be able to get the week number within the range (in this case, 5). How do I achieve this in Perl?
Package Time::Piece has strptime method to parse string into a time object and week method to indicate number of the week of the time object.
use Time::Piece;
my #dates = ('2012-01-30', ..., ...., '2012-04-06');
foreach my $strdate (#dates) {
my $date = Time::Piece->strptime($strdate, "%Y-%m-%d");
print "Number of week for '$strdate' is " . $date->week . "\n";
}
All you need to do is just to count number of unique weeks in your range.
The value of the end date doesn't make any difference unless you want to incorporate some value checking. The value that you need is week(start date) - week(this date) + 1. I recommend the Date::Calc module for its tidiness and efficiency. The code looks like this
use strict;
use warnings;
use Date::Calc 'Week_Number';
sub week {
Week_Number(split /-/, $_[0]);
}
print week('2012-02-29') - week('2012-01-30') + 1, "\n";
OUTPUT
5
use DateTime qw();
my (undef, $initial_weeknumber)
= DateTime->new(year => 2012, month => 1, day => 30)->week;
my (undef, $target_weeknumber)
= DateTime->new(year => 2012, month => 2, day => 29)->week;
printf 'We are in week %d of the Jashank epoch.',
1 + $target_weeknumber - $initial_weeknumber;

How to make DateTime::Duration output only in days?

This code finds the difference between today and a fixed date.
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
use DateTime ();
use DateTime::Duration ();
use DateTime::Format::Strptime ();
my $date = "23/05-2022";
my $parser = DateTime::Format::Strptime->new(
pattern => '%d/%m-%Y',
time_zone => 'local',
);
$date = $parser->parse_datetime($date);
my $today = DateTime->today(time_zone=>'local');
my $d = DateTime::Duration->new($today - $date);
print Dumper $d->delta_days;
The problem is that is only outputs -22 days.
If I do print Dumper $d; I can see the -130 months as well.
$VAR1 = bless( {
'seconds' => 0,
'minutes' => 0,
'end_of_month' => 'preserve',
'nanoseconds' => 0,
'days' => -22,
'months' => -130
}, 'DateTime::Duration' );
How do I get it to output the result in days?
Doing
print Dumper $d->delta_days + $d->delta_months*30;
doesn't seam like an elegant solution.
At first you need to do the correct subtraction. There exists delta_md, delta_days, delta_ms and subtract_datetime_absolute. Depending on which unit you later want, you need to pick the right subtraction. The problem is that not every unit is convertible later without time_zone information. Thats the reason why you need to pick the correct delta method.
For example a day can have 23 Hours or 24 or 25 Hours, depending on the time zone. Because of that, you need to specify how the subtraction should work. Because you want the days later, the subtraction need to focus on days, rather focus on hours. Don't use the overload feature, because it only does a best fit.
That means you need to do a delta_days subtraction.
my $dur = $date->delta_days($today);
Now $dur is a DateTime::Duration object. You need to knew that it always tries to best fit the days, weeks, years, months if possible. That means your days will split in weeks and days. Because this conversion is always a constant.
If you don't want this "best fit" you need to call the in_units method and convert it only to days.
my $days = $dur->in_units('days');
But like i said before in_units only can do a conversion where it is possible. A call with in_units('hours') will not work on this object and just return a zero because you cant convert days to hours. If you want hours for example, you need to do a delta_ms, and on this object you can call in_units('hours')
The complete example:
#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;
use DateTime;
use DateTime::Format::Strptime;
my $date = "23/05-2022";
my $parser = DateTime::Format::Strptime->new(
pattern => '%d/%m-%Y',
time_zone => 'local',
);
$date = $parser->parse_datetime($date);
my $today = DateTime->new(
day => 1,
month => 7,
year => 2011,
time_zone => 'local'
);
my $dur = $date->delta_days($today);
say "Weeks: ", $dur->weeks;
say "Days: ", $dur->days;
say "Absolute Days: ", $dur->in_units('days');
say "Absolute Hours: ", $date->delta_ms($today)->in_units('hours');
The output of this program is:
Weeks: 568
Days: 3
Absolute Days: 3979
Absolute Hours: 95496
And just for info:
1) You don't need to load DateTime::Duration its get loaded with DateTime.
2) You dont need (). These modules are OOP and don't export/import anything.
From a quick read of the DateTime module doc, I don't believe that
DateTime::Duration->new($today - $date)
will do what you expect. I believe you need to use
$dur = $today->subtract_datetime($date)
The type of $dur is not immediately clear from the docs, however.

How can I calculate the number of days between two dates in Perl?

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";