tags:

views:

1186

answers:

8

I recently "needed" a zip function in Perl 5 (while I was thinking about How do I calculate relative time?), i.e. a function that takes two lists and "zips" them together to one list, interleaving the elements.

(Pseudo)example:

@a=(1, 2, 3);
@b=('apple', 'orange', 'grape');
zip @a, @b; # (1, 'apple', 2, 'orange', 3, 'grape');

Haskell has zip in the Prelude and Perl 6 has a zip operator built in, but how do you do it in an elegant way in Perl 5?

+18  A: 

The List::MoreUtils module has a zip/mesh function that should do the trick.

Here is the source of the mesh function:

sub mesh (\@\@;\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@) {
    my $max = -1;
    $max < $#$_  &&  ($max = $#$_)  for @_;

    map { my $ix = $_; map $_->[$ix], @_; } 0..$max; 
}
Jason Navarrete
I don't know how I managed to overlook that module - thanks!
asjo
What are those escaped at-signs?
dreeves
Prototypes, saying it takes two to 32 array parameters and the sub will implicitly receive them as arrayrefs.
ysth
My eyes bleed! Isn't there some shortcut like \@{32} or something similiar?
Roberto Bonvallet
@Roberto, no but that's simply a prototype hack, your eyes are bleeding for all the wrong reason.
Evan Carroll
+1  A: 
my @l1 = qw/1 2 3/;
my @l2 = qw/7 8 9/;
my @out; 
push @out, shift @l1, shift @l2 while ( @l1 || @l2 );

If the lists are a different length, this will put 'undef' in the extra slots but you can easily remedy this if you don't wish to do this. Something like ( @l1[0] && shift @l1 ) would do it.

Hope this helps!

jonfm
Nice solution, I should probably have expressed my preference for not modifying the two input-lists :-)
asjo
+6  A: 

I can second Jason's answer. List::MoreUtils is what you're looking for. One should note that List::MoreUtils also contains an optimised C/XS-based version of this function which should be faster than the pure-Perl one.

Shlomi Fish
Seconding an answer should be a comment on the answer, not an answer in itself.
Evan Carroll
+12  A: 

Assuming you have exactly two lists and they are exactly the same length, here is a solution originally by merlyn (Randal Schwartz), who called it perversely perlish:

sub zip2 {
    my $p = @_ / 2; 
    return @_[ map { $_, $_ + $p } 0 .. $p - 1 ];
}

What happens here is that for a 10-element list, first, we find the pivot point in the middle, in this case 5, and save it in $p. Then we make a list of indices up to that point, in this case 0 1 2 3 4. Next we use map to pair each index with another index that’s at the same distance from the pivot point as the first index is from the start, giving us (in this case) 0 5 1 6 2 7 3 8 4 9. Then we take a slice from @_ using that as the list of indices. This means that if 'a', 'b', 'c', 1, 2, 3 is passed to zip2, it will return that list rearranged into 'a', 1, 'b', 2, 'c', 3.

This can be written in a single expression along ysth’s lines like so:

sub zip2 { @_[map { $_, $_ + @_/2 } 0..(@_/2 - 1)] }

Whether you’d want to use either variation depends on whether you can see yourself remembering how they work, but for me, it was a mind expander.

Aristotle Pagaltzis
wow, that's clear and concise!!!
Evan Carroll
+1  A: 

Algorithm::Loops is really nice if you do much of this kind of thing.

My own code:

sub zip { @_[map $_&1 ? $_>>1 : ($_>>1)+($#_>>1), 1..@_] }
ysth
Using bit shifts might be faster in C but is just unnecessary obfuscation in Perl. Better written like so:@_[ map { $_, $_ + @_/2 } 0 .. ( @_/2 - 1 ) ]Shorter, too.
Aristotle Pagaltzis
It isn't an issue for the question here, but my zip was designed to work for odd numbers of elements too.
ysth
A: 

This is totally not an elegant solution, nor is it the best solution by any stretch of the imagination. But it's fun!

package zip;

sub TIEARRAY {
    my ($class, @self) = @_;
    bless \@self, $class;
}

sub FETCH {
    my ($self, $index) = @_;
    $self->[$index % @$self][$index / @$self];
}

sub STORE {
    my ($self, $index, $value) = @_;
    $self->[$index % @$self][$index / @$self] = $value;
}

sub FETCHSIZE {
    my ($self) = @_;
    my $size = 0;
    @$_ > $size and $size = @$_ for @$self;
    $size * @$self;
}

sub CLEAR {
    my ($self) = @_;
    @$_ = () for @$self;
}

package main;

my @a = qw(a b c d e f g);
my @b = 1 .. 7;

tie my @c, zip => \@a, \@b;

print "@c\n";  # ==> a 1 b 2 c 3 d 4 e 5 f 6 g 7

How to handle STORESIZE/PUSH/POP/SHIFT/UNSHIFT/SPLICE is an exercise left to the reader.

ephemient
+2  A: 

Here is my version from my one-liner file:

my @zipped = ( @a, @b )[ map { $_, $_ + @a } ( 0 .. $#a ) ];
jmcnamara
Really nice solution. It takes long to understand it for me.
Hynek -Pichi- Vychodil
This has problems for unequally-sized arrays.
brian d foy
+3  A: 

I find the following solution straightforward and easy to read:

@a = (1, 2, 3);
@b = ('apple', 'orange', 'grape');
@zipped = map {($a[$_], $b[$_])} (0 .. $#a);

I believe it's also faster than solutions that create the array in a wrong order first and then use slice to reorder, or solutions that modify @a and @b.

This has problems for unequally-sized arrays.
brian d foy