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

31,789

Solution 1

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.

Solution 2

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

Solution 3

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

Solution 4

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

Solution 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.

Share:
31,789
Admin
Author by

Admin

Updated on June 29, 2020

Comments

  • Admin
    Admin almost 4 years

    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.

  • Powerlord
    Powerlord almost 15 years
    The UNIX/C epoch is January 1, 1970 00:00:00 UTC.
  • Chas. Owens
    Chas. Owens almost 15 years
    Date::Calc is not part of Core Perl
  • Michael Cramer
    Michael Cramer almost 15 years
    Obviously the mention of Date::Calc in the question implies CPAN's fair game.
  • Chas. Owens
    Chas. Owens almost 15 years
    The question also says "using default perl installation only"
  • John Siracusa
    John Siracusa almost 15 years
    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...)
  • Juan A. Navarro
    Juan A. Navarro over 12 years
    This doesn't always work accurately, see my answer.
  • tchrist
    tchrist over 12 years
    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!
  • tchrist
    tchrist over 12 years
    @John: The DST flag turning on and off is worse than leap seconds.
  • Bharat Pahalwani
    Bharat Pahalwani almost 10 years
    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";
  • basic6
    basic6 over 9 years
    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.
  • basic6
    basic6 over 9 years
    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.
  • basic6
    basic6 over 9 years
    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.
  • Juan A. Navarro
    Juan A. Navarro over 9 years
    Agreed. I understand now the source of the confusion and edited my answer hopefully to clarify much more the issue.
  • hlidka
    hlidka over 9 years
    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;
  • Juan A. Navarro
    Juan A. Navarro over 9 years
    @hlidka can you provide examples where the results come out wrong?
  • hlidka
    hlidka over 9 years
    @JuanA.Navarro here, take a look: gist.github.com/losomo/dea65735d5e6f763dc7c
  • Juan A. Navarro
    Juan A. Navarro over 9 years
    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.
  • basic6
    basic6 almost 9 years
    @JuanA.Navarro: Great to see the improved answer (which uses delta_days() now), downvote removed.