【问题标题】:Using Perl, how do I compare dates in the form of YYYY-MM-DD?使用 Perl,我如何比较 YYYY-MM-DD 形式的日期?
【发布时间】:2013-10-31 17:09:39
【问题描述】:

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

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

比如删除30天前的字符串?

【问题讨论】:

  • 为什么投了反对票?足够公平的问题,是吗?
  • 我喜欢这个问题如何引发纯粹主义者和实用主义者之间的争论。

标签: perl


【解决方案1】:

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

【讨论】:

  • 我很惊讶有这么多人跳到如此复杂的下意识的解决方案上。我们在Effective Perl Programming 中有一个项目就是这个解决方案。 :)
  • 这假定源日期中没有错误,例如2010-02-29,它将按字典顺序进行比较,但仍然是错误的日期。日期包会指出源日期错误,这似乎是脚本工作的一部分,不是吗?
  • 测试 $date gt $cutoff 将失败:1) - 以外的分隔符用于 sprintf; 2) 在应该匹配的字符串上带有前导空格; 3) 带有不应匹配的字符串的尾随空格。几乎所有的 CPAN 日期解析库都会处理这 3 种情况以及更多情况。一旦你检查了分隔符并去掉了前导和尾随空格,你真的节省了那么多“开销”或代码吗? Date::Calc 是用 C 语言编写的,速度非常快。 Date::Time 的功能非常丰富。两者都检查有效的日期输入,而此方法不检查。
  • 好吧,不要忘记所有其他可能导致所有日期模块出错的错误来源。在大多数情况下,您提到的错误来源很容易修复或发现,并且以编程方式生成时非常罕见。即使有像闰年这样的异常日期,也没有 Date 模块可以神奇地为您解决它。
【解决方案2】:

猜想有不止一种方法可以做到这一点,但我喜欢 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)

对对象进行算术运算非常棒 ++。

只有日期,没有小时、分钟或秒

【讨论】:

    【解决方案3】:
    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
    

    【讨论】:

    • 只使用my $now = DateTime-&gt;now; 不需要解析localtime 的输出。
    • 这可能会对您的程序性能造成不必要的巨大消耗。构建(单个)截止日期后,日期已经很容易按字典顺序进行比较。
    • 来吧,这是正确的解决方案。您正在描述一种可能在分析后使用的黑客并且不会适用于几乎任何其他日期格式,
    • @brian d foy:恕我直言,我不同意你的看法。我相信最好承担开销并正确处理日期。大多数 Perl 专家(包括您)都在讲授如何使用 CPAN 来获得经过良好测试的解决方案,而不是推出快速而肮脏的解决方案。 YYYY-MM-DD 的 lex 比较可能更快,但它不会捕获诸如 2010-02-29 之类的错误日期,并且它可能会遭受纪元溢出的影响。谁需要 Y2038 问题??? (2038 是 32 位 Unix 时钟溢出的时候......)将 YYYY-MM-DD 日期字符串的 lex 比较归档为酷,可能有用,但不可靠。反正也没那么容易。
    • 按字典顺序排序不是 hack。 Date 模块中也存在故障模式。尽管您的解决方案最终会产生正确的输出,但我遇到过它未能及时产生输出以使其有用的情况。
    【解决方案4】:

    一个好的开始是阅读The Many Dates of PerlDateTime 网站。

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

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

    一般来说:

    1. IFF 您不会检查日期是否有效并且 IFF 它们是相同的格式,并且 IFF 没有前导或尾随空格,您可以与另一个以相同格式表示目标日期的字符串进行比较。

    --- 否则---

    1. 决定一个 CPAN 模块来解析您的日期字符串(或自己匹配),

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

      李>
    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:恕我直言,我不同意。可以肯定的是,我这里的代码并不是那么漂亮,但是使用像 Date::Calc(用 C 语言编写)或 Date::Time 这样经过良好测试的 CPAN 库是值得的。这里的大部分工作只是生成要测试的字符串! :-} 使用您提倡的字典式解决方案,您仍然需要检查(或希望)您输入的分隔符是否相同,日期是否有效,并且没有前导或尾随空格。一旦你这样做了,它的工作量真的会减少吗?
    • 是的,我遇到过工作量少很多的情况,以及在合理时间内完成与从不完成之间的区别。
    【解决方案5】:

    你可以使用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
    

    【讨论】:

      【解决方案6】:

      我是这样做的,有点冗长,但很容易理解并完成工作。 @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;
      }
      

      跳转:

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

      【讨论】:

        【解决方案7】:

        为什么不是 5.10 以来的 CORE 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));
        

        【讨论】:

          【解决方案8】:

          在循环中查找最小日期:

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

          【讨论】:

          • 这和问题有什么关系?
          猜你喜欢
          • 2010-09-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-01-02
          • 1970-01-01
          • 1970-01-01
          • 2020-10-29
          相关资源
          最近更新 更多