views:

518

answers:

5

I have an array of arrays that looks like this:

fruits_and_calories = [
  ["apple", 100],
  ["banana", 200],
  ["kumquat", 225],
  ["orange", 90]
]

I also have a method I want to invoke on each element of the array:

fruits_and_calories.each do |f| eat(f[0], f[1])

I'd really like to be able to say something like:

fruits_and_calories.each do |f| eat(f[:name], f[:calories])

Is there a way that I can pull this off without having to change each item in the array (for example, by iterating through it and somehow adding the symbols in)? Or, if that's too hard, is there a better alternative?

A: 

An array is always indexed by numbers, so as far as I know using the standard array it's not possible.

Personally I'd just opt for using a comment above the code to hint what f[0] and f[1] stands for.

But if you are hell bent on doing it I guess some duck-typing on the Array class works:


class Array
  def name ; self[0] ; end
  def calories ; self[1] ; end
end

# then call it with:
fruits_and_calories.each {|f| eat(f.name, f.calories) }
ba
Please don't do this! Adding methods to all array instances might be useful the first time, but that way madness lies...
tomafro
I agree completely. :)
ba
A: 

Is there some reason it must be an array, per se? That seems to cry out to be a hash, or a class for Fruit.

Charlie Martin
No, there's no reason it has to be an array. But this is used in a one-off fashion in a single method and nowhere else, so a class seems like using a nuclear-powered rotary saw where I only wanted a hammer.
Kyle Kaitan
If I could use a hash without having to specify "this one is the name, this one is the calories" for every single element, that would be an excellent alternative too. Can that be done?
Kyle Kaitan
Yes, see Peslo's answer. You sould also use "apple" => 100 of course.
Charlie Martin
("so a class seems like using a nuclear-powered rotary saw...") Kyle, are you saying Hash is a class and Array is not? In Ruby they are both classes.
Walt Gordon Jones
He means "write a new class." you could write a class with :attr fruit, calories and then make an array of them.
Charlie Martin
+8  A: 

The best answer is not to use an Array at all, but to use a Hash:

fruits_and_calories = { :apple => 100,
                        :banana => 200,
                        :kumquat => 225,
                        :orange => 90}
fruits_and_calories.each do |name, calories| 
  eat(name, calories)
end
Pesto
Actually, the key is the "|name, calories|" part, which works the same with his original array of arrays...
glenn mcdonald
Yes, an array would work, but the design is begging for a hash.
Pesto
+2  A: 

Pesto's answer (use a hash) is a good one, but I think I'd prefer to use a Struct.

Fruit = Struct.new(:name, :calories)

fruits = [
  Fruit.new("apple", 100),
  Fruit.new("banana", 200),
  Fruit.new("kumquat", 225),
  Fruit.new("orange", 90)
]

fruits.each {|f| eat(f.name, f.calories)}

This also lends itself to changing eat from taking both the name and calories, to taking a fruit instance:

fruits.each {|f| eat(f)}
tomafro
+5  A: 

Without changing the data structure at all, you could change the block arguments to achieve the same thing:

fruits_and_calories.each  do |name, calories|  eat(name, calories);  end

This works because Ruby will auto-expand the inner arrays (["apple", 100], etc) into the argument list for the block ('do |name, calories| ... end'). This is a trick that Ruby inherited from Lisp, known as 'destructuring arguments'.

Kevin