Pergunta

I have an array of hashes:

connections = [
{:name=>"John Doe", :number=>"5551234567", :count=>8},
{:name=>"Jane Doe", :number=>"5557654321", :count=>6},
{:name=>"John Doe", :number=>"5559876543", :count=>3}
]

If the :name value is a duplicate, as is the case with John Doe, it should combine the :number values into an array. The count is not important anymore, so the output should be in the following format:

{"John Doe"=>["5551234567","5559876543"],
"Jane Doe"=>["5557654321"]}

What I have so far is:

k = connections.inject(Hash.new{ |h,k| h[k[:name]] = [k[:number]] }) { |h,(k,v)| h[k] << v ; h }

But this only outputs

{"John Doe"=>["5559876543", nil], "Jane Doe"=>["5557654321", nil]}
Foi útil?

Solução

This works:

connections.group_by do |h| 
  h[:name] 
end.inject({}) do |h,(k,v)| 
  h.merge( { k => (v.map do |i| i[:number] end) } ) 
end
# => {"John Doe"=>["5551234567", "5559876543"], "Jane Doe"=>["5557654321"]}

Step by step...

connections is the same as in your post:

connections
# => [{:name=>"John Doe", :number=>"5551234567", :count=>8}, 
#     {:name=>"Jane Doe", :number=>"5557654321", :count=>6}, {:name=>"John Doe",
#      :number=>"5559876543", :count=>3}]

First we use group_by to combine the hash entries with the same :name:

connections.group_by do |h| h[:name] end
# => {"John Doe"=>[{:name=>"John Doe", :number=>"5551234567", :count=>8}, 
#                  {:name=>"John Doe", :number=>"5559876543", :count=>3}], 
#     "Jane Doe"=>[{:name=>"Jane Doe", :number=>"5557654321", :count=>6}]}

That's great, but we want the values of the result hash to be just the numbers that show up as values of the :number key, not the full original entry hashes.

Given just one of the list values, we can get the desired result this way:

[{:name=>"John Doe", :number=>"5551234567", :count=>8}, 
 {:name=>"John Doe", :number=>"5559876543", :count=>3}].map do |i| 
  i[:number] 
end
# => ["5551234567", "5559876543"]

But we want to do that to all of the list values at once, while keeping the association with their keys. It's basically a nested map operation, but the outer map runs across a Hash instead of an Array.

You can in fact do it with map. The only tricky part is that map on a Hash doesn't return a Hash, but an Array of nested [key,value] Arrays. By wrapping the call in a Hash[...] constructor, you can turn the result back into a Hash:

Hash[ 
  connections.group_by do |h|
    h[:name] 
  end.map do |k,v| 
    [ k, (v.map do |i| i[:number] end) ] 
  end
]

That returns the same result as my original full answer above, and is arguably clearer, so you might want to just use that version.

But the mechanism I used instead was inject. It's like map, but instead of just returning an Array of the return values from the block, it gives you full control over how the return value is constructed out of the individual block calls:

connections.group_by do |h| 
  h[:name] 
end.inject({}) do |h,(k,v)| 
  h.merge( { k => (v.map do |i| i[:number] end) } ) 
end

That creates a new Hash, which starts out empty (the {} passed to inject), and passes it to the do block (where it shows up as h) along with the first key/value pair in the Hash returned by group_by. That block creates another new Hash with the single key passed in and the result of transforming the value as we did above, and merges that into the passed-in one, returning the new value - basically, it adds one new key/value pair to the Hash, with the value transformed into the desired form by the inner map. The new Hash is returned from the block, so it becomes the new value of h for the next time through the block.

(We could also just assign the entry into h directly with h[k] = v.map ..., but the block would then need to return h afterward as a separate statement, since it is the return value of the block, and not the value of h at the end of the block's execution, that gets passed to the next iteration.)

As an aside: I used do...end instead of {...} around my blocks to avoid confusion with the {...} used for Hash literals. There is no semantic difference; it's purely a matter of style. In standard Ruby style, you would use {...} for single-line blocks, and restrict do...end to blocks that span more than one line.

Outras dicas

In one line:

k = connections.each.with_object({}) {|conn,result| (result[conn[:name]] ||= []) << conn[:number] }

More readable:

result = Hash.new {|h,k| h[k] = [] }
connections.each {|conn| result[conn[:name]] << conn[:number] }

result #=> {"John Doe"=>["5551234567", "5559876543"], "Jane Doe"=>["5557654321"]}
names = {}
connections.each{ |c| names[c[:name]] ||= []; names[c[:name]].push(c[:number]) }
puts names
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top