I have a replication process which applies updates. Usually the updates are incremental 'patches' sometimes they are full copies. The incoming queue of updates might look like this:
my @files = qw( 00000.diff 00001.diff 00002.diff 00003.copy 00004.diff 00005.diff 00006.copy 00007.diff );
If there are one or more full copies in the queue, there is no point processing any updates before the last full copy. So what I needed was an algorithm to reduce the above example to just the last two files.
My first thought was that I ought to be able to do it with some form of grep using the flip-flop operator. Alas, my initial attempts were not successful so I wrote a loop that popped things off one array and unshifted them onto another - breaking out of the loop if a full copy file was found. It wasn't complicated code but it took more lines than I'd have liked and it needed a one-line comment explaining what the loop was doing (perhaps, in retrospect, I should have put it into a subroutine).
I later realised what I was doing wrong in my first attempts and came up with this version:
my $i = 0; @files = reverse grep !$i++../\.copy$/, reverse @files;
Having to use $i was annoying but I couldn't think of another way to code an expression that was true the first time it was evaluated and false from then on.
Anyway, I ran this version past a couple of my colleagues and while they were intrigued, they felt it was basically unreadable and didn't want to be saddled with maintaining it. So I 'committed' it here instead :-)
my @keep;
foreach (@files) {
@keep = () if/\.copy$/;
push @keep, $_;
}
Re:My shot
grantm on 2006-03-21T18:34:00
Of all the suggestions, this one definitely gets my vote for clarity. I did think along the lines of discarding accumulated filenames whenever a '.copy' was encountered but at the time I had grep blinkers on and it can't be done with grep.
use List::MoreUtils qw/before_incl/;
my @keep =
reverse
before_incl {/\.copy\z/ }
reverse @files;
...So what I needed was an algorithm to reduce the above example to just the last two files....
Or, at least, you want to get the last index of a
my $index = (
grep({$files[$_] =~ m/\.copy$/}
reverse(0..$#files)
)
)[0];
splice(@files, 0, $index);
List::Util improves this
Aristotle on 2006-03-21T12:29:58
use List::Util qw( first );
my $index = first { $files[ $_ ] =~ m/\.copy$/ } reverse 0.. $#files;
splice @files, 0, $index if defined $index;
my $seen_full_copy;
@files = reverse grep { not( $seen_full_copy ||=/\.copy$/ ) } reverse @files;
The double reverse
is still unsightly though.
use strict;
sub fold {
my $accum = shift;
my $code = shift;
return $accum unless @_;
return fold( $code->( $accum, shift ), $code, @_ );
}
my @files = qw(
00000.diff
00001.diff
00002.diff
00003.copy
00004.diff
00005.diff
00006.copy
00007.diff
00008.diff
);
use Data::Dumper;
sub foldee {
my ( $accum, $item ) = @_;
return [$item, []] if ( $item =~ /\.copy$/ );
return $accum unless defined $accum->[ 0 ];
return [ $accum->[ 0 ], [@{$accum->[1]}, $item]];
}
print Dumper(fold( [undef, []], \&foldee, @files));