简体   繁体   中英

How do I read two items at a time in a Perl foreach loop?

What I'm looking for is something like:

@list = qw(1 2 3 4 5 6);
foreach (@list) {
  #perl magic goes here 
  print "i: $i, j:$j\n";
}

returns:

i:1, j:2
i:3, j:4
i:5, j:6

In response to a very good suggestion below, I need to specify that this script will run on someone else's build server, and I'm not allowed to use any modules from CPAN. Standard Perl only.

I believe the proper way to do this is to use natatime, from List::MoreUtils :

from the docs:

natatime BLOCK LIST

Creates an array iterator, for looping over an array in chunks of $n items at a time. ( n at a time, get it?). An example is probably a better explanation than I could give in words.

Example:

 my @x = ('a' .. 'g');
 my $it = natatime 3, @x;
 while (my @vals = $it->())
 {
     print "@vals\n";
 }

This prints

a b c
d e f
g

The implementation of List::MoreUtils::natatime :

sub natatime ($@)
{
    my $n = shift;
    my @list = @_;

    return sub
    {
        return splice @list, 0, $n;
    }
}

I'd use splice.

my @list = qw(1 2 3 4 5 6);
while(my ($i,$j) = splice(@list,0,2)) {
  print "i: $i, j: $j\n";
}

I think you'd want to do this differently. Try this:

while (scalar(@list) > 0) {
    $i = shift(@list);
    $j = shift(@list);
    print "i: $i, j:$j\n";
} 

Keep in mind that this will destroy the list, but it will work for that little loop.

Set up some test data, and import say :

use Modern::Perl;
use List::AllUtils qw'zip';

my @array = zip @{['a'..'z']}, @{[1..26]} ;

Simple looping using an increment variable.

    {
      my $i = 0;
      while(
        (my($a,$b) = @array[$i++,$i++]),
        $i <= @array # boolean test
      ){
        say "$a => $b";
      }
    }

Looping over pairs using List::Pairwise (pair) .

    use List::Pairwise qw'pair';

    for my $pair (pair @array){
      my($a,$b) = @$pair;

      say "$a => $b";
    }

Looping over array 2 at a time, using List::MoreUtils (natatime) .

    use List::AllUtils qw'natatime';

    my $iter = natatime 2, @array;
    while( my($a,$b) = $iter->() ){
      say "$a => $b";
    }

Coerce it into a hash, and loop over the keys. Useful if you don't care about the order.

    {
      my %map = @array;
      for my $key (keys %map){
        my $value = $map{$key};
        say "$key => $value";
      }
    }

The closest equivalent is, unfortunately, going old-school:

for(my $ix = 0; $ix <= $#list; $ix += 2) {
    my $i = $list[$ix];
    my $j = $list[$ix + 1];
    print "i: $i, j:$j\n";
}

I like Jack M's answer better, really, though I would write it in sexier Perl:

while(@list) {
    my $i = shift @list;
    my $j = shift @list;
    print "i: $i, j:$j\n";
}

If I only could use standard Perl with no modules, I'd probably drop down to a C-style for loop that counts by 2:

for( my $i = 0; $i &lt; @array; $i += 2 ) {
    my( $j, $k ) = @array[ $i, $i+1 ];
    ...
    }

If you have an odd number of elements, you'll have to decide how to handle an extra element. Your problem may not care that you get an extra element that is undefined since you specifically need pairs.

Simply reading past the end of an array does not change the array, so that part is fine.

If you must have pairs, a simple tactic might be to add an appropriate value to the end of the array so you always end up with pairs. Likewise, you can remove the last element (or whichever element) to end up with an even number again. Those depend on your problem.

Otherwise, you're doing slightly more work:

for( my $i = 0; $i < @array; $i += 2 ) {
    push @pair, $array[$i];
    push @pair, $array[$i+1] if $i+1 <= $#array;
    ... 
    }

However, if you wanted something fancy from one of the modules you can't use, you can just add that module to your code. If you can write code, you can use modules. You might just have to include the module with all of the code you deliver while you set @INC appropriately. This is the basic idea of inc::Module::Install and PAR .

I spend a lot of my time working with a build system that creates its own CPAN repository, installs its dependencies from its private CPAN, and then tests code. Having a build farm doesn't preclude using modules; it's local policy that does. However, that might not make sense in all cases even though it's possible.

Risking the necromancy tag, I decided to add one more from Tim Toady's backpack:

for (0 .. $#list) {
    next if $_ % 2;
    my ($i, $j) = @list[$_, $_ + 1];
    say "i:$i, j:$j";
}

Nondestructive, no duplicate lists, no state variables and reasonably terse.

You will probably want to create a simple subroutine to make it work for you.

I suggest this:

{
  my $cl_ind = 0;
  sub arrayeach(@) {
    my @obj = @_;
    if(($cl_ind+2) > @obj)
    {
      $cl_ind = 0;
      return;
    }
    $cl_ind+=2;
    return ($obj[$cl_ind-2],$obj[$cl_ind-1]);
  }
}

The closure makes it work cleanly. To use arrayeach (which works like the hash each without requiring dangerous coercion to an array:

my @temp = (1,2,3,4,5,6,1,2,3,4,5,6);
while( ($a,$b) = arrayeach(@temp)) {
  print "A $a AND $b\n";
}

This is nondestructive.

How about a general purpose functional solution.

use Carp; # so mapn can croak about errors

sub mapn (&$@) {
    my ($sub, $n, @ret) = splice @_, 0, 2;
    croak '$_[1] must be >= 1' unless $n >= 1;
    while (@_) {
        local *_ = \$_[0];
        push @ret, $sub->(splice @_, 0, $n)
    }
    @ret
}

sub by ($@) {mapn {[@_]} shift, @_}
sub every ($@); *every = \&by;

The mapn function works just like map , except the first argument after its block is the number of elements to take. It places the first element in $_ and all of the elements in @_ .

print mapn {"@_\n"} 2 => 1 .. 5;
# prints
1 2
3 4
5

The next two identical subs, by and every create useful adverbs for the various looping constructs. They process the list with mapn, and return a list of array refs of the desired size

print "@$_\n" for every 2 => 1..10;

print map {"@$_\n"} grep {$_->[1] > 5} by 2 => 1..10;

I find this to be a cleaner and more intuitive solution than natatime, or other one off solutions like ac style for loop.

my $i;
for ( qw(a b c d) ) {
    if (!defined($i)) { $i = $_; next; }
    print STDOUT "i = $i, j = $_\n";
    undef($i);
}

Outputs:

i = a, j = b
i = c, j = d

It also works for lists, not only for arrays.

As Mirod explains, there isn't much code to it. Here's pretty much all you would need. (Note that I don't have any checks for odd-numbered lists or the like.)

#!/usr/bin/env perl
use strict;
use warnings;

my @list = qw/1 2 3 4 5 6/;
my $get_em = get_by(2, @list);

while ( my ($i, $j) = $get_em->() ) {
  print "i: $i, j: $j\n";
}

sub get_by {
  my $n = shift;
  my @list = @_;

  return sub {
    return splice @list, 0, $n;
  }
}

quick solution for small arrays:

for ( map {$_*2} 0..@list/2-1 ){
    my ($i, $j) = @list[$_,$_+1];
    print "i: $i, j:$j\n";
}

some kind oneliner

data:

@v = (a=>1, b=>2, c=>3);

this

print join ', ', map{sprintf '%s:%s', $v[$_], $v[$_+1]} grep {!($_%2)} 0..$#v

or somthing like this

print join ', ', map {sprintf '%s:%s', @v[$_,$_+1]} map {$_*2} 0..@v/2-1;

result is same

a:1, b:2, c:3

another approach, not fully clean, but usable. each creates iterator, you can use it twice. when parameter is classic array, it returns index and value, please read this: https://perldoc.perl.org/functions/each.html

so, your code can be like this:

my @array=qw(one two three four five); #five element as unpaired will be ignored
while (my ($i1,$one,$i2,$two)=(each(@array),each(@array)) {
  #we will use $ix for detect end of array
  next unless defined $i1 and defined $i2; #secure complete end of array
  print "fetched array elements: $one => $two\n";
};

Example above will not destruct source data, against shift or similar. I hope this we helpful for anyone. of course case with plain iterator is much better.

Using a for loop would do what you need.

use strict;
use warnings;

my @list = qw(1 2 3 4 5 );
my $i = 0;

for ($i = 0; $i < scalar(@list); $i++)
{
    my $a = $list[$i];
    my $b = $list[++$i];
    if(defined($a)) {
        print "a:$a";
    }
    if(defined($b)) {
        print "b:$b";
    }   
    print "\n";
}

edit : I corrected my post to use the scalar function to retrieve the size of the array and also add some checking in case the array does not contain an even number of elements.

This can be done non-destructively, with Eric Strom's simply fantastic List::Gen :

perl -MList::Gen=":utility" -E '@nums = "1" .. "6" ; 
      say "i:$_->[0] j:$_->[1]" for every 2 => @nums'

Output :

i:1 j:2 
i:3 j:4 
i:5 j:6 

Edit (add a CPAN-less version):

Array slices and C-style for loop à la brian d foy and Tom Christiansen ! This can be read as "use an index ( $i ) to loop through a @list foreach $n elements at a time":

use v5.16; # for strict, warnings, say

my @list = "1" .. "6";
my $n = 2 ;   # the number to loop by
$n-- ;        # subtract 1 because of zero index

foreach (my $i = 0 ; $i < @list ; $i += $n ) { 
  say "i:", [ @list[$i..$i+$n] ]->[0], " j:", [ @list[$i..$i+$n] ]->[1];
  $i++ ;          
}

We access the results as elements ( ->[0] ) of an anonymous array ( [ ] ). For more generic output the interpolated array slice could be used on its own, eg : print "@list[$i..$i+$n]"; changing the value of $n as required.

I came up with this code to solve a similar requirement:

sub map_pairs(&\@) {
    my $op = shift;
    use vars '@array';
    local *array = shift;    # make alias of calling array

    return () unless @array;

    # Get package global $a, $b for the calling scope
    my ($caller_a, $caller_b) = do {
        my $pkg = caller();
        no strict 'refs';
        \*{$pkg.'::a'}, \*{$pkg.'::b'};
    };

    # Get index counter size.
    my $limit = $#array/2;

    # Localize caller's $a and $b
    local(*$caller_a, *$caller_b);

    # This map is also the return value
    map {
        # assign to $a, $b as refs to caller's array elements
        (*$caller_a, *$caller_b) = \($array[$_], $array[$_+1]);
        $op->();    # perform the transformation
    } 
    map { 2 * $_ } 0..$limit;  # get indexes to operate upon.
}

You use it like so:

@foo = qw( a 1 b 2 c 3 );
my @bar = map_pairs { "$a is $b" } @foo;

to get:

@bar = ( 'a is 1', 'b is 2', 'c is 3' );

I've been meaning to submit to the maintainer of List::MoreUtils, but I don't have an XS version to offer.

here's an implementation of natatime that doesn't make a copy of the list:

sub natatime {
  my $n = shift;
  my $list = \@_;

  sub {
    return splice @$list, 0, $n;
  }
}

my $it = natatime(3, qw(1 2 3 4 5 6));
while ( my @list = $it->() ) {
  print "@list\n";
}

Divide number of elements by two, iterate by one. Print index and next:

print "i:$list[$_*2], j:$list[$_*2+1]\n" for 0..$#list/2;

Or if you want a full block:

foreach my $i ( 0..$#list/2 ) { print "i:$list[$i*2], j:$list[$i*2+1]\n"; }

Or:

foreach my $n ( 0..$#list/2 ) { 
    my ($i,$j) = @list[$n,$n+1];
    print "i:$i, j:$j\n"; 
}

I think more simpler way is to use old poor 'each'. Straight like this:

 while (my ($key,$value) = each @list) { print "$key=$value\\n"; }

Updated:

Yes, it's wrong. One should convert list to hash first but it could be too exensive:

my %hash = (@list);
while (my ($key,$value) = each %hash) {
        print "$key=$value\n";
}
while ($#rec>0) {
  my($a,$b) = ( shift(@rec), shift(@rec) );
  print "($a,$b)\n";
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM