如何使用 Perl 比较形如 YYYY-MM-DD 的日期?

13

我有一个包含n个字符串的数组,格式为YYYY-MM-DD(例如,“2010-10-31”)。

如何将日期与此数组中的字符串进行比较?

例如,删除30天前的字符串?


1
为什么被投票否决了?公平的问题,是吗? - Jé Queue
2
我喜欢这个问题引发了纯粹主义者和实用主义者之间的争论。 - paddy
8个回答

16
YYYY-MM-DD格式的日期非常好比较,只需要用简单的字符串比较就可以了。在Perl中,使用ltgt操作符进行比较。
在这种情况下,看起来你只是想检查数组中的日期是早于还是晚于给定的目标日期(这个日期恰好是“30天前”)。对于这种情况,我会先确定30天前的日期,然后将其作为字符串与数组中的每个日期进行比较。我不会为了测试哪个日期表示更早而引入将所有YYYY-MM-DD字符串转换为“正确”的日期对象、时期时间等的开销。
#!/usr/bin/env perl

use strict;
use warnings;

my $thirty_days = 30 * 24 * 60 * 60;
my ($old_day, $old_month, $old_year) = (localtime(time - $thirty_days))[3..5];
my $cutoff = sprintf('%04d-%02d-%02d', 
                     $old_year + 1900, $old_month + 1, $old_day);

my @dates = ('2010-10-12', '2010-09-12', '2010-08-12', '2010-09-13');
for my $date (@dates) {
  print "$date\n" if $date gt $cutoff;
} 

3
很惊讶有这么多人采用复杂的、膝反射式的解决方案。我们在《Effective Perl Programming》中就有一个这样的解决方案。 :) - brian d foy
这假设源日期没有错误,例如2010-02-29,虽然在字典上比较但仍是错误的日期。一个日期包会指出源日期错误,这似乎应该是脚本的一部分,不是吗? - dawg
测试$date gt $cutoff将失败,原因如下:1)在sprintf中使用除“-”以外的分隔符;2)字符串前面有空格,但应该匹配;3)字符串后面有空格,但不应该匹配。几乎所有的CPAN日期解析库都可以处理这三种情况,以及许多其他情况。一旦检查了分隔符并去掉了前导和尾随空格,你真的节省了那么多“开销”或代码吗?Date::Calc是用C编写的,非常快。Date::Time功能非常丰富。两者都检查有效的日期输入,而这种方法则不会。 - dawg
2
不要忘记所有可能会使日期模块出错的其他来源。大多数情况下,你提到的错误来源很容易修复或发现,并且在程序生成时非常罕见。即使是像错误的闰年这样的异常日期,也没有任何日期模块可以为你神奇地解决它。 - brian d foy

8

猜测有很多方法可以做到这一点,但我喜欢Date::Simple来处理这种事情。

以下是文档中的一个例子:

use Date::Simple ('date', 'today');

# Difference in days between two dates:
$diff = date('2001-08-27') - date('1977-10-05');

# Offset $n days from now:
$date = today() + $n;
print "$date\n";  # uses ISO 8601 format (YYYY-MM-DD)

它非常适用于对对象++进行算术运算。

只限日期,不包括小时、分钟或秒数。


5
use strict; use warnings;
use DateTime ();
use DateTime::Duration ();
use DateTime::Format::Natural ();

my $parser = DateTime::Format::Natural->new;
my $now    = DateTime->now;
my $delta  = DateTime::Duration->new( days => 30 );
my $cutoff = $now->subtract_duration( $delta );

my @new_dates = map  { $_->[1] }
                grep { -1 == $_->[0] }
                map  { 
                    chomp;
                    [
                        DateTime->compare(
                            $parser->parse_datetime( $_ ),
                            $cutoff
                        ),
                        $_ 
                    ]
                } <DATA>;

print "@new_dates";

__DATA__
2010-07-31
2010-08-31
2010-09-30
2010-10-31

1
只需使用 my $now = DateTime->now;,无需解析 localtime 的输出。 - daotoad
3
这可能会极大地拖慢程序的性能,是一种不必要的负担。只要构建(单个)截止日期,就已经很容易按字典顺序比较这些日期了。 - brian d foy
1
加油,这是正确的解决方案。你正在描述一种可能仅适用于其他日期格式化的削减方法。 - Pedro Silva
@brian d foy:恕我直言,我不同意您的观点。我认为最好承受一些额外开销并正确处理日期。大多数Perl专家(包括您)都会讲授使用CPAN进行经过充分测试的解决方案,而不是编写快速而肮脏的解决方案。YYYY-MM-DD的词法比较可能更快,但它无法捕捉到错误的日期,例如2010-02-29,并且可能会遭受时期溢出的问题。谁需要Y2038问题?(2038年是32位Unix时钟溢出的时间...)将YYYY-MM-DD日期字符串的词法比较归档为酷、可能有用,但不够健壮。反正也没有那么容易。 - dawg
2
按字典序排序并不是一种hack。日期模块中也存在故障模式。尽管你的解决方案最终会产生正确的输出,但我曾经遇到过这种情况,它无法及时产生有用的输出。 - brian d foy

1
我是这样做的,有点啰嗦但很容易理解并且能完成任务。@out2 是一个二维数组,我使用 for 循环读入值。每次循环,我将输入与 @out2 进行比较,以查看它是否为早期或晚期的时间/日期。如果是,则将值写入数组,然后再比较下一个输入。
if ($year < $out2[$j][7]) {
  $lt = 1;
  goto JUMP;
}
if ($year > $out2[$j][7]) {
  $gt = 1;
  goto JUMP;
}
if ($month < $out2[$j][5]) {
  $lt = 1;
  goto JUMP;
}
if ($month > $out2[$j][5]) {
  $gt = 1;
  goto JUMP;
}
if ($day < $out2[$j][6]) {
  $lt = 1;
  goto JUMP;
}
if ($day > $out2[$j][6]) {
  $gt = 1;
  goto JUMP;
}
if ($time < $out2[$j][4]) {
  $lt = 1;
  goto JUMP;
}
if ($time > $out2[$j][4]) {
  $gt = 1;
  goto JUMP;
}

JUMP:

if ($lt == 1) {
  $out2[$j][2] = "$dtime $month\/$day\/$year";
  $out2[$j][4] = $time;
  $out2[$j][5] = $month;
  $out2[$j][6] = $day;
  $out2[$j][7] = $year;
  $lt = 0;
  }

if ($gt == 1) {
  $out2[$j][3] = "$dtime $month\/$day\/$year";
  $out2[$j][4] = $time;
  $out2[$j][5] = $month;
  $out2[$j][6] = $day;
  $out2[$j][7] = $year;
  $gt = 0;
}

1

一个好的开始是阅读Perl的许多日期DateTime网站。

YYYY-MM-DD格式是ISO 8601日期表示的一种形式。有些变体被认为是可接受的,例如YYYY-MM-DDYYYYMMDD甚至是旧数据中的YYMM。在选择比较这些日期的方法之前,您应该查看权威列表

如果ISO 8601日期字符串:1)是有效日期;2)具有相同的格式,带或不带-分隔符;3)缺少前导和尾随空格,则一个吸引人的特性是您可以使用简单的字典字符串比较对字符串进行排序或比较。

总的来说:

  1. 如果你不打算检查日期是否有效,并且它们具有相同的格式,并且没有前导或尾随空格,那么你可以将其与另一个表示目标日期的字符串进行比较,该字符串具有相同的格式。

--- 否则 ---

  1. 决定使用CPAN模块解析日期字符串(或自己匹配),

  2. 如果您的日期在该范围内,请转换为时期时间(或使用Date::Manip或Date::Calc等执行更大规模的日期/时间操作的CPAN模块)

  3. 对于时间类型(时期时间、绝对天数等),执行算术运算

  4. 将时间转换回所需的格式...

以下是执行此操作的代码:

use warnings; use strict;
use Date::Calc qw/:all/;

my (@date_strings, @abs_days);

my $target=Date_to_Days(2010, 1, 15);

# set @date_string to "YYYY-MM-DAY" between some dates
for my $AbsDay(Date_to_Days(2009,1,1)..Date_to_Days(2011,12,31)) {
   my ($year, $mon, $day)=Add_Delta_Days(1,1,1,$AbsDay-1);
   my $s="$year-$mon-$day";
   push @date_strings, $s;
}

foreach my $s (@date_strings) {
    my ($year, $mon, $day);

    if(($year, $mon, $day)=$s=~/^(\d+)-(\d+)-(\d+)/) {
        my $days=Date_to_Days($year, $mon, $day);
        push @abs_days, $days 
             if ($target-$days <= 30 && $target-$days >= -30 );
    }
}

print "absolute day way:\n";
foreach my $days (@abs_days) {
    my ($year, $mon, $day)=Add_Delta_Days(1,1,1,$days-1);
    print "$year-$mon-$day\n";
}

@brian d foy:恕我直言,我不同意。我的代码并不是那么漂亮,但使用经过充分测试的 CPAN 库,比如 Date::Calc(用 C 写成)或 Date::Time,值得这种开销。这里大部分工作只是生成要测试的字符串! :-} 采用您提倡的词典排序解决方案,仍然需要检查(或希望)输入具有相同的分隔符,是有效日期,并且没有前导或尾随空格。一旦你做到了这一点,它真的比较省事吗? - dawg
1
是的,我曾经遇到过这种情况,有时候工作量会少很多,这就是合理时间内完成和永远无法完成的区别。 - brian d foy

1

您可以使用Time::ParseDate模块,

use strict;
use warning;
use Time::ParseDate;

my @dates = ('2010-10-12', '2010-09-14', '2010-08-12', '2010-09-13');
my @dates = 
  grep {parsedate($_, NO_RELATIVE => 1, UK => 1) > parsedate('-30 days') }@dates;   
 #output: 2010-10-12 2010-09-14

0

为什么不使用CORE自5.10以来的Time::PieceTime::Seconds,而不是CPAN搜索结果中的前几个?

use strict;
use warnings;
use Time::Piece (); # we don't need to include overloaded locatime
use Time::Seconds;
use Data::Dumper;

my @dates = qw/2010-10-31 2012-10-16 2011-09-08/;

my $now = Time::Piece::localtime();

my @date_objects = map {
    Time::Piece->strptime( $_, '%F')  # %F is the same as %Y-%m-%d 
} @dates;

my @filtered_dates = grep {
    $now - $_ < (ONE_DAY * 30)
} @date_objects;

print Dumper(map($_->strftime('%F'), @filtered_dates));

0
在循环中找到最小日期:
var minDate = ...;
var maxDate = ...;

foreach my $date ( @$dates ) {
    if ($minDate gt $date){ # Less than.
        $minDate = $date; # Minimal date.
    }
    if ($minDate lt $date){ # Greater than.
        $minDate = $date; # Maxamal date.
    }
}

这与问题有什么关系? - Toto

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