1

我认为我需要某种Schwartzian Transform才能使其正常工作,但我无法弄清楚,因为 perl 不是我最强的语言。

我有一个目录,内容如下:

album1.htm
album2.htm
album3.htm
....
album99.htm
album100.htm

我正在尝试从此目录中获取编号最高的专辑(在本例中为album100.htm)。请注意,文件上的时间戳并不是确定事物的可靠方法,因为人们会在事后添加旧的“丢失”专辑。

之前的开发者只是简单地使用了下面的代码片段,但是一旦目录中的专辑超过 9 个,这显然会崩溃。

opendir(DIR, PATH) || print $!;
@files = readdir(DIR);
foreach $file ( sort(@files) ) {
    if ( $file =~ /album/ ) {
        $last_file = $file;
    }
}
4

6 回答 6

7

如果您只需要找到编号最高的专辑,您实际上不需要对列表进行排序,只需遍历它并跟踪最大值即可。

#!/usr/bin/perl 

use strict;
use warnings;

my $max = 0;

while ( <DATA> ) {
    my ($album) = $_ =~ m/album(\d+)/;
    $max = $album if $album > $max;
}

print "album$max.htm";

__DATA__
album1.htm
album100.htm
album2.htm
album3.htm
album99.htm
于 2010-06-02T18:52:31.077 回答
3

要找到最高的数字,请尝试自定义排序...

sub sort_files {
    (my $num_a = $a) =~ s/^album(\d+)\.htm$/$1/;
    (my $num_b = $b) =~ s/^album(\d+)\.htm$/$1/;
    return $num_a <=> $num_b;
}

my @sorted = sort \&sort_files @files;
my $last = pop @sorted;

另外,看看File::Next模块。它会让您只挑选以“专辑”开头的文件。我发现它比readdir容易一些。

于 2010-06-02T19:04:41.733 回答
2

您遇到困难的原因是运算符,<=>是数字比较,cmp默认值,是字符串比较。

$ perl -E'say for sort qw/01 1 02 200/';
01
02
1
200

稍作修改,我们得到更接近正确的东西:

$ perl -E'say for sort { $a <=> $b } qw/01 1 02 200/';
01
1
02
200

但是,在您的情况下,您需要删除非数字。

$ perl -E'say for sort { my $s1 = $a =~ m/(\d+)/; my $s2 = $b =~ /(\d+)/; $s1 <=> $s2  } qw/01 1 02 200/';
01
1
02
200

这里更漂亮:

sort {
  my $s1 = $a =~ m/(\d+)/;
  my $s2 = $b =~ /(\d+)/;
  $s1 <=> $s2
}

这并非完美无缺,但它应该让您对排序问题有一个很好的了解。

哦,作为后续,Shcwartzian 变换解决了一个不同的问题:它使您不必在搜索算法中多次运行一项复杂的任务(与您需要的任务不同——一个正则表达式)。它的代价是必须缓存结果(不要出乎意料)。本质上,您所做的是将问题的输入映射到输出(通常在数组中),[$input, $output]然后对输出进行排序$a->[1] <=> $b->[1]。现在你的东西排序你映射回来得到你原来的输入$_->[0]

map $_->[0],
sort { $a->[1] <=> $b->[1] }
map [ $_, fn($_) ]
, qw/input list here/
;

它很酷,因为它既紧凑又高效。

于 2010-06-02T19:18:25.943 回答
1

这是一个通用的解决方案:

my @sorted_list
    = map  { $_->[0] } # we stored it at the head of the list, so we can pull it out
      sort {
          # first test a normalized version
          my $v = $a->[1] cmp $b->[1];
          return $v if $v;

          my $lim = @$a > @$b ? @$a : @$b;

          # we alternate between ascii sections and numeric
          for ( my $i = 2; $i < $lim; $i++ ) {
              $v  =  ( $a->[$i] || '' ) cmp ( $b->[$i] || '' );
              return $v if $v;

              $i++;
              $v = ( $a->[$i] || 0 ) <=> ( $b->[$i] || 0 );
              return $v if $v;
          }
          return 0;

      }
      map {
          # split on digits and retain captures in place.
          my @parts = split /(\d+)/;
          my $nstr  = join( '', map { m/\D/ ? $_ : '0' x length() } @parts );
          [ $_, $nstr, @parts ];
      } @directory_names
      ;
于 2010-06-02T20:35:00.030 回答
1

在这里,使用 Schwartzian 变换:

my @files = <DATA>;

print join '',
    map  { $_->[1] }
    sort { $a->[0] <=> $b->[0] }
    map  { [ m/album(\d+)/, $_ ] }
    @files;


 __DATA__
album12.htm
album1.htm
album2.htm
album10.htm
于 2010-06-02T19:28:14.203 回答
1

这是使用reduce的替代解决方案:

use strict;
use warnings;
use List::Util 'reduce';

my $max = reduce {
    my ($aval, $bval) = ($a =~ m/album(\d+)/, $b =~ m/album(\d+)/);
    $aval > $bval ? $a : $b
} <DATA>;
print "max album is $max\n";

__DATA__
album1.htm
album100.htm
album2.htm
album3.htm
album99.htm
于 2010-06-02T19:47:26.747 回答