在Perl中如何计算两个日期之间的天数?

23

我希望能够使用默认的 Perl 安装程序计算两个日期之间的天数,这两个日期的格式都像这样:04-MAY-09 (DD-MMM-YY)。

我找不到任何介绍这种日期格式的教程。我应该为此格式构建一个自定义日期检查器吗?阅读 CPAN 上的 Date::Calc 的更多信息,看起来不太可能支持这种格式。


2
你能不能将日期转换为秒,然后计算它们之间的差异呢? - TStamper
1
请不要重复计算秒数的错误。请参考我的回答中的示例。 - basic6
7个回答

20

看起来有些混淆,因为根据您想要实现的目标,“两个日期之间的天数”可能意味着至少两种不同的东西:

  1. 两个日期之间的日历距离
  2. 两个日期之间的绝对距离

以一个例子来说明并注意其区别,假设您有两个如下构造的DateTime对象:

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

请注意,$dt1 是星期二的很晚,而$dt2 是紧随其后的星期五非常早。
如果您想要日历距离,请使用:
my $days = $dt2->delta_days($dt1)->delta_days();
print "$days\n" # -> 3

实际上,周二和周五之间有三天的时间间隔。日历距离为1表示“明天”,距离为-1表示“昨天”。DateTime对象中的“时间”部分大多是无关紧要的(除非两个日期处于不同的时区,那么您必须决定这两个日期之间的“日历距离”应该意味着什么)。

如果您想要绝对距离,则应使用以下方法:

my $days = $dt2->subtract_datetime_absolute($dt1)->delta_seconds / (24*60*60);
print "$days\n"; # -> 2.06916666666667

事实上,如果你想按照24小时的间隔将两个日期之间的时间分割开,那么它们之间只有大约2.07天的时间。根据你的应用,你可能想要截断或舍入这个数字。DateTime对象的“时间”部分非常相关,即使是不同时区的日期也有清晰定义的预期结果。


1
我不确定在将天数四舍五入到零时调用int是否正确,但由于它取决于他对其的使用方式,我也不知道什么是正确的。有时我可以看到想要floor()(对于正数给出与您得到的相同),有时是ceil(),有时是sprintf "%.0f",有时只需保留浮点数。但这些都可以根据他的需求进行简单调整。困难的是说你不能使用CPAN,但可以征求代码。发现:该代码要求他将所有DateTime复制并粘贴到程序中,这是最糟糕的代码重用方式。唉! - tchrist
1
另外,请解释为什么 days, delta_days 或 in_units('days') 不起作用 - 你提到了 subtract_datetime_absolute,但它计算的是秒和纳秒的绝对差异,这不是我们想要的。 - basic6
1
对于“日历距离”,my $days = $dt2->delta_days($dt1)->days; 对我来说返回了一个错误的数字,我需要将其更改为 my $days = $dt2->delta_days($dt1)->delta_days; - hlidka
@hlidka,你能否提供一些结果错误的示例? - Juan A. Navarro
1
@JuanA.Navarro:很高兴看到改进的答案(现在使用delta_days()),取消了踩。 - basic6
显示剩余4条评论

19

如果你关心准确性,请记住并不是所有的一天都有86400秒。基于这个假设的任何解决方案对某些情况来说都不正确。

这里有一个片段,我保留它来使用DateTime库以几种不同的方式计算和显示日期/时间差异。我认为打印的最后一个答案就是你想要的。

#!/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";

1
这并不总是准确的,参见我的答案 - Juan A. Navarro
这确实可以准确地工作(已点赞)。subtract_datetime_absolute()调用可能返回不是86400的倍数的秒数(例如255600!=(3 * 86400 = 259200)在[我的示例](https://dev59.com/CnRA5IYBdhLWcg3wzhRY#26732755)中)。delta_md()调用返回实际的天数差异。一开始可能会感到困惑,因为您正在使用$ dur变量来处理2个不同的事物(绝对时间差异和日历日期差异)。操作员要求天数,因此日期差异是您答案中有趣的部分。 - basic6

5

Time::ParseDate可以很好地处理这种格式:

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

1
Time::ParseDate 不是 Perl 核心的一部分。 - Chas. Owens
2
显然,问题中提到的Date::Calc意味着CPAN是公平竞争的。 - Michael Cramer
1
问题还说“仅使用默认的Perl安装”。 - Chas. Owens
如果月份、日期等的值长度为1,则无法正常工作。即$d1="2014-5-29 9:0:00";和$d2="2014-5-29 10:0:00"; - Bharat Pahalwani

5

Date::Calc拥有Decode_Date_EU(以及US等)功能

#!/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);

1
Date::Calc不是Perl核心的一部分。 - Chas. Owens

5
这个问题已经有一个不错的答案了,但是我想提供一个回答,展示为什么在使用格式化/本地日期而不是浮动日期时计算秒数是错误的

我发现很多建议告诉人们要减去秒数,这让我感到不安。(这个问题是我的搜索中第一个谷歌搜索结果,所以我不在乎它有多老。)

我自己也犯过这个错误,想知道应用程序为什么会突然(在周末)显示不正确的时间。因此,我希望这段代码能帮助那些可能面临这种问题的人理解为什么这种方法是错误的,并帮助他们避免这个错误。

下面是一个完整的示例,没有在某个关键点处包含“...”(因为如果您在同一时区插入两个日期,您可能看不到错误)。

#!/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";

输出:

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)

注意:

  • 我使用的是本地日期。如果你使用浮动日期,你就不会有这个问题——因为你的日期保持在同一个时区。
  • 我的示例中,两个时间范围都从星期五到星期一,所以天数差为3,而不是3.04…当然也不是2.95…
  • 使用int()将浮点数转换为整数(正如一个答案中所建议的)是错误的,正如这个示例所显示的那样。
  • 我意识到将秒的差值舍入也会在我的示例中返回正确的结果,但是我觉得这仍然是错误的。你会计算出一个为2的天数差值(对于大值而言),并且,因为它是一个较大的2,将其转换为3。所以只要DateTime提供了这种功能,就使用DateTime。

引用文档(delta_days()与subtract_datetime()):

日期与日期时间的运算

如果您只关心日期(日历)部分的日期时间,您应该使用delta_md()或delta_days(),而不是subtract_datetime()。这将提供可预测的、没有与DST相关的复杂性的结果。

总之:如果你使用DateTime,请不要比较秒数。如果你不确定要使用哪个日期框架,请使用DateTime,它非常棒。


1

将这两个日期转换为秒,然后进行数学计算:

#!/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";

1
你可以将日期转换为长整型格式,即自1970年某个日期以来的秒数。然后你就有了两个变量,它们是以秒为单位的日期;将较小的日期从较大的日期中减去。现在你有了一个以秒为单位的时间跨度;将其除以24小时的秒数,就得到了时间跨度的天数。

UNIX/C纪元是1970年1月1日00:00:00 UTC。 - Powerlord
这将为您提供两个日期之间86400秒时间块的数量,这与两个日期之间(日历)天数的数量并不相同。(好吧,实际上甚至由于闰秒,它甚至不是86400秒块的相同内容...) - John Siracusa
@John:DST标志的开启和关闭比闰秒更糟糕。 - tchrist

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接