Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpectedly short Perl slices

Tags:

list

slice

perl

The following snippet of Perl is supposed to print the first 5 items of an array referenced by a hash value, or fewer if the array is shorter.

while ( my ($key,$value) = each %groups ) {
   print "$key: \n";
   my @list = grep defined, @{$value};
   my @slice = grep defined, @list[0..4];
   foreach my $item ( @slice ) {
      print "   $item \n";
   }
   print "   (", scalar @slice, " of ", scalar @list, ")\n";
}

I don't think the first grep defined is necessary, but it can't do any harm, and it should guarantee that there are no undefined array members before the slice. The second grep defined is to remove undefined array members in the result of slice when @list is shorter than 5.

%groups has been populated by repeated invocations of:

  $groups{$key} = () unless defined $groups{$key};
  push @{$groups{$key}}, $value;

Most of the time it works fine:

key1:
   value1
   value2
   value3
   value4
   value5
   (5 of 100)

But sometimes -- and I haven't worked out under what circumstances -- I see:

key2:
   value1
   (1 of 5)

key3:
   value1
   value2
   (2 of 5)

I expect the length of the printed list, and x from (x of y) to be min(5,y)

What could cause this behaviour?

like image 978
slim Avatar asked May 08 '13 15:05

slim


1 Answers

Using grep with an array slice for @list autovivifies the elements and extends the array.

@foo = (1,2,3);
@bar = @foo[0..9999];
print scalar @foo;             # =>  3

@foo = (1,2,3);
@bar = grep 1, @foo[0..9999];
print scalar @foo;             # => 10000

This also happens in other contexts where Perl wants to loop over an array slice.

@foo = (1,2,3);
foreach (@foo[0..9999]) { }
print scalar @foo;             # => 10000

@foo = (1,2,3);
@bar = map { } @foo[0..9999];
print scalar @foo;             # => 10000

So what are the workarounds?

  1. Use a more complicated expression for the range or the grep operand

    @bar = grep 1, @foo[0..(@foo>=9999?9999:$#foo)];
    @bar = grep 1, @foo>=9999 ? @foo[0..9999] : @foo;
    
  2. Use a temporary array variable

    @bar = grep 1, @tmp=@foo[0..9999]
    
  3. (suggested by @FMc) use map to set up an intermediate array

    @bar = grep 1, map { $list[$_] } 0..9999;
    
  4. work with array indices rather than directly with the array

    @bar_indices = grep defined($foo[$_]), 0..9999;
    @bar = @foo[@bar_indices];
    
    @bar = @foo[  grep defined($foo[$_]), 0..9999 ];
    
like image 167
mob Avatar answered Oct 22 '22 07:10

mob