def things_n_stuff

Adventures in Code

Ruby's Zip Method

Always start by reading the ruby docs. The .zip method is implemented in the Array class and Enumberable module, below, here.

Basics

.zip is useful for combining collections in an ordered way. We’ll first call it with two collections, one as the message receiver and one as the message’s argument: array.zip(other_array) The order is irrelevant for now. .zip will return arrays made by combining the collections piece by piece.

So far, simple:

1
2
[1,2,3].zip(['a', 'b', 'c'])
#=> [[1, "a"], [2, "b"], [3, "c"]]

.zip can also take 2 (or more) arguments to zip into the first:

1
2
['a', 'b', 'c'].zip( [1,2,3], ['oogie', 'boogie', 'booger'] )
#=> [["a", 1, "oogie"], ["b", 2, "boogie"], ["c", 3, "booger"]]

What it’s doing

.zip walks over each of the collections, collecting one element from each and returning the resulting collected array. It then repeats that process until it’s made one pass over each element from the collections. The resulting arrays from each of these passes is collected and returned as an array of those collected arrays. I find this illustration helpful:

1
2
3
4
['FIRST-1', 'FIRST-2', 'FIRST-3'].zip( ['SECOND-1','SECOND-2','SECOND-3'], ['THIRD-1', 'THIRD-2','THIRD-3'] )
#=> [  ["FIRST-1", "SECOND-1", "THIRD-1"], 
       ["FIRST-2", "SECOND-2", "THIRD-2"],
       ["FIRST-3", "SECOND-3", "THIRD-3"]  ]

Unequal collections, returned array lengths

If the argument collection sizes are unequal, .zip places nils in the zipped up arrays:

1
2
3
4
['FIRST-1', 'FIRST-2', 'FIRST-3'].zip( ['SECOND-1'], ['THIRD-1', 'THIRD-2'] )
#=> [  ["FIRST-1", "SECOND-1", "THIRD-1"],
       ["FIRST-2", nil, "THIRD-2"],
       ["FIRST-3", nil, nil]  ]
Careful:

The number of arrays returned by .zip is determined by the length of the receiver collection. Here, the receiver ['FIRST-1', 'FIRST-2'] has a length of 2 items. Notice that the ['SECOND-3'] item doesn’t get zipped-in or included at all:

1
2
3
['FIRST-1', 'FIRST-2'].zip( ['SECOND-1','SECOND-2','SECOND-3'], ['THIRD-1'] )
#=> [  ["FIRST-1", "SECOND-1", "THIRD-1"], 
       ["FIRST-2", "SECOND-2", nil]   ]

… and here again, the receiver has a length of 1, so the zipped array has a length of 1 regardless of the length of its args. The result is an array that contains one zipped array of content collected from each collection:

1
2
['FIRST-1'].zip( ['SECOND-1','SECOND-2','SECOND-3'], ['THIRD-1', 'THIRD-2','THIRD-3', 'THIRD-4'] )
#=> [  ["FIRST-1", "SECOND-1", "THIRD-1"]  ]

Back to text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
(from ruby core)
=== Implementation from Array
------------------------------------------------------------------------------
  ary.zip(arg, ...)                   -> new_ary
  ary.zip(arg, ...) {| arr | block }  -> nil

------------------------------------------------------------------------------

Converts any arguments to arrays, then merges elements of self with
corresponding elements from each argument. This generates a sequence of
self.size n-element arrays, where n is one more that the count of
arguments. If the size of any argument is less than enumObj.size, nil values
are supplied. If a block is given, it is invoked for each output array,
otherwise an array of arrays is returned.

  a = [ 4, 5, 6 ]
  b = [ 7, 8, 9 ]
  [1,2,3].zip(a, b)      #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
  [1,2].zip(a,b)         #=> [[1, 4, 7], [2, 5, 8]]
  a.zip([1,2],[8])       #=> [[4,1,8], [5,2,nil], [6,nil,nil]]


(from ruby core)
=== Implementation from Enumerable
------------------------------------------------------------------------------
  enum.zip(arg, ...)                   -> an_array_of_array
  enum.zip(arg, ...) {|arr| block }    -> nil

------------------------------------------------------------------------------

Takes one element from enum and merges corresponding elements from
each args.  This generates a sequence of n-element arrays, where n
is one more than the count of arguments.  The length of the resulting sequence
will be enum#size.  If the size of any argument is less than enum#size, nil
values are supplied. If a block is given, it is invoked for each output array,
otherwise an array of arrays is returned.

  a = [ 4, 5, 6 ]
  b = [ 7, 8, 9 ]

  [1,2,3].zip(a, b)      #=> [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
  [1,2].zip(a,b)         #=> [[1, 4, 7], [2, 5, 8]]
  a.z
Extra credit

Because .zip is implemented in the Enumerable module also, you can call .zip on anything you turn into an Enumerator object (ie., anything that you call enumerable methods (to which it responds) on without a block) in the same way.

So, while:

1
2
3
4
5
6
7
8
9
10
11
12
'eyyo'.zip('ummmm')
#=>NoMethodError: undefined method `zip' for "eyyo":String

# so make 'eyyo' into an Enumerator, and it'll behave just like an array of it's elements

'eyyo'.chars.zip('ummmm')
#=>NoMethodError: undefined method `each' for "ummmm":String

# ah, but awesome ruby says the arg needs to be an enumerator object too, so:

'eyyo'.chars.zip('ummmm'.chars)
#=> [["e", "u"], ["y", "m"], ["y", "m"], ["o", "m"]]

And a last example for good measure. The following two are equivalent, and remember that both would return nil:

1
2
3
4
5
6
7
8
9
10
11
'wct'.chars.zip( 'ohh'.chars, 'wei'.chars, 'zcs'.chars, 'ak'.chars ){|x| puts x.join }
  wowza
  check
  this
#=> nil
-------
['w','c','t'].zip( ['o','h','h'],['w','e','i'],['z','c','s'], ['a','k']){|zipped_array| puts zipped_array.join}
  wowza
  check
  this
#=> nil

You’d think there’d be an unzip method too, but doesn’t seem so.

Anyway, go play!

For a different take with practical examples, testing, and source code, check out this post.