Reputation: 109
I am new to programing and am starting with Ruby. Using .each ONLY, my challenge is to turn this:
animals = {
'leopard' => 1,
'gorilla' => 3,
'hippo' => 4,
'zebra' => 1,
'lion' => 2,
'eagle' => 3,
'ostrich' => 2,
'alligator' => 6
}
Into this hash where the animals are grouped by count:
animals_by_count = {
1 => ['leopard', 'zebra'],
2 => ['lion', 'ostrich'],
3 => ['gorilla', 'eagle'],
4 => ['hippo'],
6 => ['alligator']
}
This is my code, it is not working properly. Again, I must use .each ONLY, I my NOT use .map .inject .select or any another method.
animals_by_count = {}
animals.each do |animal, count|
animals_array = []
if !animals_by_count.has_key?(count)
animals_by_count[count] = animals_array << animal
elsif animals_by_count.has_key?(count)
animals_by_count[count] = animals_array << animal
end
end
Upvotes: 0
Views: 68
Reputation: 110675
@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:
animals
one at a timeBuild 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) }
Upvotes: 2
Reputation: 118271
You can do as below :-
animals = {
'leopard' => 1,
'gorilla' => 3,
'hippo' => 4,
'zebra' => 1,
'lion' => 2,
'eagle' => 3,
'ostrich' => 2,
'alligator' => 6
}
animals_by_count = {}
animals.each do |animal, count|
if animals_by_count.has_key?(count)
animals_by_count[count].push(animal)
else
animals_by_count[count] = [animal]
end
end
animals_by_count
# => {1=>["leopard", "zebra"],
# 3=>["gorilla", "eagle"],
# 4=>["hippo"],
# 2=>["lion", "ostrich"],
# 6=>["alligator"]}
Why not worked your one ?
animals.each do |animal, count|
animals_array = []
# see here at every iteration, you are creating a new empty array and using it.
# animals_by_count key thus holding the last array, loosing the previous one.
Upvotes: 2