27

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?

Community
  • 1
  • 1
asjo
  • 3,084
  • 2
  • 26
  • 20
  • Haskell's zip is not what you are looking for: it returns a list of corresponding pairs, not a list of interleaved elements. – Chris Conway Jan 28 '09 at 02:04
  • You're right; Haskell lists contains elements if a single type. I wasn't thinking when I referred to Haskell here. – asjo Feb 14 '09 at 00:39
  • 4
    Often when one thinks they want a zip, it's to create a hash from two lists. In that case better to use a hash slice. `@hash{@keys} = @values`. If that's not the case here, then sorry for the noise. – Joel Berger Jan 04 '14 at 20:23

7 Answers7

37

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
  • 112,955
  • 23
  • 98
  • 97
30

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

use List::MoreUtils qw(zip);

my @numbers = (1, 2, 3);
my @fruit = ('apple', 'orange', 'grape');

my @zipped = zip @numbers, @fruit;

Here is the source of the mesh function:

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

    map { my $ix = $_; map $_->[$ix], @_; } 0..$max; 
}
ThisSuitIsBlackNot
  • 23,492
  • 9
  • 63
  • 110
Jason Navarrete
  • 7,495
  • 6
  • 28
  • 22
14

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.

Frank
  • 64,140
  • 93
  • 237
  • 324
  • 1
    This has problems for unequally-sized arrays. – brian d foy Jan 03 '10 at 17:47
  • 1
    @briandfoy your comments help, but you know what you forgot to do? point out which one *does* work for unequally-sized arrays. (I'm shopping for one that needs to do this) – Steven Lu Aug 27 '13 at 22:12
  • I didn't forget. That problem depends on what you want to do with the remaining elements. You should ask a different question and specify your constraints. – brian d foy Aug 28 '13 at 14:38
10

For arrays of the same length:

my @zipped = ( @a, @b )[ map { $_, $_ + @a } ( 0 .. $#a ) ];
jmcnamara
  • 38,196
  • 6
  • 90
  • 108
4
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
  • 249
  • 2
  • 4
  • 1
    Nice solution, I should probably have expressed my preference for not modifying the two input-lists :-) – asjo Sep 16 '08 at 22:01
2

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..@_] }
Alan Haggai Alavi
  • 72,802
  • 19
  • 102
  • 127
ysth
  • 96,171
  • 6
  • 121
  • 214
  • 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 Sep 21 '08 at 21:46
  • 1
    It isn't an issue for the question here, but my zip was designed to work for odd numbers of elements too. – ysth Sep 22 '08 at 07:44
1

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
  • 198,619
  • 38
  • 280
  • 391