tags:

views:

535

answers:

4

What's a better way to traverse an array while iterating through another array? For example, if I have two arrays like the following:

names = [ "Rover", "Fido", "Lassie", "Calypso"]
breeds = [ "Terrier", "Lhasa Apso", "Collie", "Bulldog"]

Assuming the arrays correspond with one another - that is, Rover is a Terrier, Fido is a Lhasa Apso, etc. - I'd like to create a dog class, and a new dog object for each item:

class Dog
  attr_reader :name, :breed

  def initialize(name, breed)
    @name = name
    @breed = breed
  end
end

I can iterate through names and breeds with the following:

index = 0

names.each do |name|
  Dog.new("#{name}", "#{breeds[index]}")
  index = index.next
end

However, I get the feeling that using the index variable is the wrong way to go about it. What would be a better way?

+16  A: 
dogs = names.zip(breeds).map { |name, breed| Dog.new(name, breed) }

Array#zip interleaves the target array with elements of the arguments, so

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

You can use arrays of different lengths (in which case the target array determines the length of the resulting array, with the extra entries filled in with nil).

irb> [1, 2, 3, 4, 5].zip(['a', 'b', 'c'])
 #=> [ [1, 'a'], [2, 'b'], [3, 'c'], [4, nil], [5, nil] ]
irb> [1, 2, 3].zip(['a', 'b', 'c', 'd', 'e'])
 #=> [ [1, 'a'], [2, 'b'], [3, 'c'] ]

You can also zip more than two arrays together:

irb> [1,2,3].zip(['a', 'b', 'c'], [:alpha, :beta, :gamma])
 #=> [ [1, 'a', :alpha], [2, 'b', :beta], [3, 'c', :gamma] ]

Array#map is a great way to transform an array, since it returns an array where each entry is the result of running the block on the corresponding entry in the target array.

irb> [1,2,3].map { |n| 10 - n }
 #=> [ 9, 8, 7 ]

When using iterators over arrays of arrays, if you give a multiple parameter block, the array entries will be automatically broken into those parameters:

irb> [ [1, 'a'], [2, 'b'], [3, 'c'] ].each { |array| p array }
[ 1, 'a' ]
[ 2, 'b' ]
[ 3, 'c' ]
#=> nil
irb> [ [1, 'a'], [2, 'b'], [3, 'c'] ].each do |num, char| 
...>   puts "number: #{num}, character: #{char}" 
...> end
number 1, character: a
number 2, character: b
number 3, character: c
#=> [ [1, 'a'], [2, 'b'], [3, 'c'] ]

Like Matt Briggs mentioned, #each_with_index is another good tool to know about. It iterates through the elements of an array, passing a block each element in turn.

irb> ['a', 'b', 'c'].each_with_index do |char, index| 
...>   puts "character #{char} at index #{index}"
...> end
character a at index 0
character b at index 1
character c at index 2
#=> [ 'a', 'b', 'c' ]

When using an iterator like #each_with_index you can use parentheses to break up array elements into their constituent parts:

irb> [ [1, 'a'], [2, 'b'], [3, 'c'] ].each_with_index do |(num, char), index| 
...>   puts "number: #{num}, character: #{char} at index #{index}" 
...> end
number 1, character: a at index 0
number 2, character: b at index 1
number 3, character: c at index 2
#=> [ [1, 'a'], [2, 'b'], [3, 'c'] ]
rampion
+1, yours is better
Matt Briggs
+1 `zip` http://ruby-doc.org/core/classes/Array.html#M002198
OscarRyz
+2  A: 

each_with_index leaps to mind, it is a better way to do it the way you are doing it. rampion has a better overall answer though, this situation is what zip is for.

Matt Briggs
+1: definitely a good tool to know about, rather than maintaining your own index variable.
rampion
+2  A: 

This is adapted from Flanagan and Matz, "The Ruby Programming Language", 5.3.5 "External Iterators", Example 5-1, p. 139:

++++++++++++++++++++++++++++++++++++++++++

require 'enumerator'  # needed for Ruby 1.8

names = ["Rover", "Fido", "Lassie", "Calypso"]  
breeds = ["Terrier", "Lhasa Apso", "Collie", "Bulldog"]

class Dog  
    attr_reader :name, :breed  

    def initialize(name, breed)  
        @name = name  
        @breed = breed  
    end  
end

def bundle(*enumerables)  
    enumerators = enumerables.map {|e| e.to_enum}  
    loop {yield enumerators.map {|e| e.next} }  
end  

bundle(names, breeds) {|x| p Dog.new(*x) }  

+++++++++++++++++++++++++++++++++++++++++++

Output:

#<Dog:0x10014b648 @name="Rover", @breed="Terrier">  
#<Dog:0x10014b0d0 @name="Fido", @breed="Lhasa Apso">  
#<Dog:0x10014ab80 @name="Lassie", @breed="Collie">  
#<Dog:0x10014a770 @name="Calypso", @breed="Bulldog">  

which I think is what we wanted!

Jan Hettich
this is a very good solution as well, and has the added bonus of returning the objects and their attributes. thanks!
michaelmichael
That loop made my head spin until some research revealed how it works. Per Pickaxe, "loop silently rescues the StopIteration exception, which works well with external iterators." Neat!
Wayne Conrad
+1  A: 

As well as each_with_index (mentioned by Matt), there's each_index. I sometimes use this because it makes the program more symmetrical, and therefore wrong code will look wrong.

names.each_index do |i|
  name, breed = dogs[i], breeds[i] #Can also use dogs.fetch(i) if you want to fail fast
  Dog.new(name, breed)
end
Andrew Grimm