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

7

23

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.

Avron answered 4/5, 2009 at 19:0 Comment(2)
can't you just convert the dates to seconds, and calculate the difference?Oversubtlety
Please don't repeat the mistake of calculating the difference in seconds. See my answer for an example.Janson
R
20

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:

  1. The calendar distance between the two dates.
  2. 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.

Revolt answered 18/8, 2011 at 17:40 Comment(9)
I'm not sure that calling int on the number of days to round toward zero is right, but since it depends what he is doing with it, I wouldn't know what is right, either. I can see sometimes wanting the floor() (which for positives gives the same as you got), sometimes the ceil(), sometimes sprintf "%.0f", and sometimes just keeping the float. But those are easy to adjust according to his needs. The hard thing is saying you can't use CPAN but can solicit code. Find: the code requires that he suck all of DateTime into his program with cut and paste, the worst kind of code reuse. Alas!Luau
I downvoted - going from winter to summer, the difference in seconds / 86400 may be 2.95, which is NOT 3. Cutting of the fraction (what int() does) turns 2.95 into 2, which is not 3 and therefore wrong. See my answer for an example that demonstrates this.Janson
Also, please explain why days, delta_days or in_units('days') wont work - you mention subtract_datetime_absolute, but that calculates the absolute difference in seconds and nanoseconds, which is not what we want.Janson
Agreed. I understand now the source of the confusion and edited my answer hopefully to clarify much more the issue.Revolt
For calendar distance, my $days = $dt2->delta_days($dt1)->days; returned a wrong number for me, I needed to change it to my $days = $dt2->delta_days($dt1)->delta_days;Risner
@Risner can you provide examples where the results come out wrong?Revolt
@JuanA.Navarro here, take a look: gist.github.com/losomo/dea65735d5e6f763dc7cRisner
You are absolutely right, I'll fix the answer. DateTime::Duration days() and weeks() need to be used together in order to make any sense. In this context, however, delta_days() is clearly what we need. I'm surprised how long it passed before anybody noticing! For myself, I guess I always used this operation to compare two dates very near to each other.Revolt
@JuanA.Navarro: Great to see the improved answer (which uses delta_days() now), downvote removed.Janson
C
19

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";
Chaunce answered 4/5, 2009 at 20:57 Comment(2)
This doesn't always work accurately, see my answer.Revolt
This does work accurately (upvoted). The subtract_datetime_absolute() call may return a number of seconds that is not a multiple of 86400 (like 255600 != (3* 86400 = 259200) in my example). The delta_md() call returns the actual difference in days. It may be confusing at first that you're using the $dur variable for 2 different things (absolute time difference and calendar date difference). The op asked for days, so the date difference is the interesting part of your answer.Janson
G
5

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);
Gounod answered 4/5, 2009 at 19:25 Comment(4)
Time::ParseDate is not part of Core PerlMisappropriate
Obviously the mention of Date::Calc in the question implies CPAN's fair game.Gounod
The question also says "using default perl installation only"Misappropriate
won't work if month,day etc value is of length = 1. i.e., $d1="2014-5-29 9:0:00"; and $d2="2014-5-29 10:0:00";Aglet
B
5

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);
Bier answered 4/5, 2009 at 19:54 Comment(1)
Date::Calc is not part of Core PerlMisappropriate
J
5

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.

Janson answered 4/11, 2014 at 10:23 Comment(0)
H
1

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.

Hydrostatic answered 4/5, 2009 at 19:6 Comment(3)
The UNIX/C epoch is January 1, 1970 00:00:00 UTC.Bathe
This will give you the number of 86400-seconds blocks of time between two dates, which is not the same thing as the number of (calendar) days between two dates. (Okay, it's actually not even the same thing as 86400-second blocks either, due to leap seconds...)Chaunce
@John: The DST flag turning on and off is worse than leap seconds.Luau
M
1

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";
Misappropriate answered 4/5, 2009 at 20:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.