@Arup has explained why your code is not working, and showed you have to fix it. I would like show you some variations and alternatives that an experienced Rubiest might used. These fall into two categories:
Build a new hash
This is the approach you have taken, which @Arup fixed. Let me change Arup's code slightly:
h = {}
animals.each do |(name, num)|
h[num] = [] unless h.key?(num)
h[num] << name
end
h
#=> {1=>["leopard", "zebra"],
# 3=>["gorilla", "eagle"],
# 4=>["hippo"],
# 2=>["lion", "ostrich"], 6=>["alligator"]}
Whenever h
does not contain a key num
, we add the element num => []
to the hash. Then name
is appended to the array h[num]
. The statement:
h[num] = [] unless h.key?(num)
is the same as
h[num] = h[num] || []
which can be combined with the next line:
h[num] = (h[num] || []) << name
and optionally written
(h[num] ||= []) << name
If we use the last of these, we would write:
h = {}
animals.each { |(name,num)| (h[num] ||= []) << name }
h
Rather than creating the empty hash, building it in the next step and the returning it after it's built, that can all be done in one line:
animals.each_with_object({}) { |(name,num),h| (h[num] ||= []) << name }
A variant of this is to replace the empty hash with the form of Hash#new that takes a block:
Hash.new { |h,k| h[k] = [] }
If this hash is accessed by a key that doesn’t correspond to a hash entry, h
and k
are passed to the block to allow you to create a value for the key. Here we want that to be an empty array. This allows us to write:
animals.each_with_object(Hash.new { |h,k| h[k] = [] }) { |(name,num),h|
h[num] << name }
I'm not suggesting which of these approaches you should use. They all work. The choice is yours.
Enumerable#each_with_object was introduced in Ruby v1.9. Before that, and still now, Enumerable#reduce (aka inject
) would or can be used for this task:
animals.reduce({}) { |h,(name,num)| (h[num] ||= []) << name; h }
Notice the order of the block parameters: |(name,num),h|
for each_with_object
, |h,(name,num)|
for reduce
. Also, when using reduce
, the object (here h
) must be returned to reduce
. That's the reason for the lonely h
as the last statement in reduce
's block.
Use Enmerable#goup_by
The second common way to attack this kind of problem is to use group_by
, which returns the following hash:
h = animals.group_by { |_,v| v }
#=> {1=>[["leopard", 1], ["zebra", 1]],
# 3=>[["gorilla", 3], ["eagle", 3]],
# 4=>[["hippo", 4]],
# 2=>[["lion", 2], ["ostrich", 2]],
# 6=>[["alligator", 6]]}
Now all we need to do is to manipulate the hash values (e.g., change [["leopard", 1], ["zebra", 1]]
to ["leopard", "zebra"]
.
There are a couple of ways to do this. One is to enumerate over the hash keys and modify the values:
h.keys.each { |k| h[k] = h[k].map(&:first) }
h
Another is to build a new hash:
g = {}
h.each { |k,v| g[k] = v.map(&:first) }
g
which can also be written:
h.each_with_object({}) { |(k,v),g| g[k] = v.map(&:first) }