tags:

views:

844

answers:

12

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.

+9  A: 

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.

Jack M.
This is list-destructive. Huge difference on his request
@cmartin, nothing in his request suggests that it can't use up the list. So, list-destructive actions might fit the bill. But, if that's the case, there's no reason to shift-shift, just splice.
Axeman
I didn't test, but doesn't the second shift fail when there are an odd number of elements in @list? But what's an odd undef among friends. ;-)
Jon Ericson
+6  A: 

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";
}
chaos
+1 for the standard for() loop. Some control structures are a classic because they work =).
jj33
@jrockway, who edited $ix <= $#list to $ix < @list so SO's syntax highlighting would work better: I know you mean well, but the idea of changing the actual content (not formatting) of code so a Web site displays it better inspires me to great violence.
chaos
@chaos - from @jrockway's edit comment, the change wasn't for syntax highlighting, it was for readability and "idiomatic-ness". It works just the same and has two less punctuation marks. How can you argue with that? :-)
Brian Phillips
He said it was to 'fix markdown markup', which is kind of unclear since Markdown as such isn't involved, but the nearest thing that seemed to have anything to do with it was the # being interpreted as a comment.
chaos
Anyway, I don't really accept the alternate version as 'more idiomatic'. Two characters shorter is something I'll usually go for, but for whatever crotchety reason, in this case since Perl lets me say exactly what I mean, I'd rather do that at the cost of two characters.
chaos
I never actually use $#array to get the last element of the array. So I prefer $ix < @list.
Brad Gilbert
That is certainly your privilege, sir.
chaos
To clarify: $array[$#array] is the last element (same as $array[-1]), not $#array.
J.F. Sebastian
+8  A: 

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";
}
Andrew Barnett
As noted on another answer, this is also list-destructive. The non-destructive answer would involve a for loop with indexes and doing index + 1.
Andrew Barnett
That's my choice too, if I didn't care about destroying the list. +1.
Axeman
If list destruction is a problem, you can just copy the list beforehand. You can even do it inside a { block } so it falls out of scope when you're done: { my @temp = @list; while(my ($i, $j) = splice(@list,0,2)) { print "i: $i, j: $j\n"; } }
Chris Lutz
+1  A: 

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.

Pierre-Luc Simard
+1  A: 

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.

And what happen if I want consume two array in parallel?
Hynek -Pichi- Vychodil
That wasn't the question. But it's wonderful to see I have more negative points than solutions that are generally considered "worse".
+21  A: 

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;
    }
}
mirod
This rocks. Anything is better than a c-style for-loop, but this is MUCH better.
innaM
I've gotta get that off of CPAN, don't I?Sad to say that this script runs on a build server I don't own.That's really nice though, thank you.
Sean Cavanagh
As an other comment states, it's one of the "very-perlish" answer: "grab a module" ;--)so to be complete: use List::MoreUtils qw(natatime);my @list = qw(1 2 3 4 5 6);my $it = natatime 2, @list;while( my( $i, $j)= $it->()) { print "i: $i, j:$j\n";}
mirod
You don't have to install it system-wide necessarily. You can just copy it locally, or even just grab the code (look at http://cpansearch.perl.org/src/VPARSEVAL/List-MoreUtils-0.22/lib/List/MoreUtils.pm ) it's super-short!
mirod
Yep, that'll do it.Thanks!
Sean Cavanagh
Notice that there is little bit difference between foreach and natatime. foreach is faster and less memory consuming (doesn't copy array as natatime does internally) and allow array members modification e.g foreach my $a (@x) { $a++ } is same as $_++ foreach @x
Hynek -Pichi- Vychodil
natatime wouldn't need to copy the list if it did: my $list = \@_; then later: splice @$list, 0, $n;
runrig
+2  A: 

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;
  }
}
Telemachus
+3  A: 

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";
      }
    }
Brad Gilbert
Thanks for the pointer to List::Pairwise.
daotoad
A: 

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.

daotoad
+3  A: 

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 < @array; $i += 2 )
    {
    my( $i, $j ) = @array[ $i, $i+1 ];
    ...
    }

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.

Good luck, :)

brian d foy
A: 

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";
}
runrig
A: 

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 a c style for loop.

Eric Strom