Merge hashes based on particular key/value pair in ruby

岁酱吖の 提交于 2020-01-06 17:59:33

问题


I am trying to merge an array of hashes based on a particular key/value pair.

array = [ {:id => '1', :value => '2'}, {:id => '1', :value => '5'} ]

I would want the output to be

{:id => '1', :value => '7'}

As patru stated, in sql terms this would be equivalent to:

SELECT SUM(value) FROM Hashes GROUP BY id

In other words, I have an array of hashes that contains records. I would like to obtain the sum of a particular field, but the sum would grouped by key/value pairs. In other words, if my selection criteria is :id as in the example above, then it would seperate the hashes into groups where the id was the same and the sum the other keys.

I apologize for any confusion due to the typo earlier.


回答1:


Edit: The question has been clarified since I first posted my answer. As a result, I have revised my answer substantially.

Here are two "standard" ways of addressing this problem. Both use Enumerable#select to first extract the elements from the array (hashes) that contain the given key/value pair.

#1

The first method uses Hash#merge! to sequentially merge each array element (hashes) into a hash that is initially empty.

Code

def doit(arr, target_key, target_value)
  qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
  return nil if qualified.empty?  
  qualified.each_with_object({}) {|h,g|
    g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
end

Example

arr = [{:id  => '1', :value => '2'}, {:id => '2', :value => '3'},
       {:id  => '1', :chips => '4'}, {:zd => '1', :value => '8'},
       {:cat => '2', :value => '3'}, {:id => '1', :value => '5'}]

doit(arr, :id, '1')
  #=> {:id=>"1", :value=>"7", :chips=>"4"}

Explanation

The key here is to use the version of Hash#merge! that uses a block to determine the value for each key/value pair whose key appears in both of the hashes being merged. The two values for that key are represented above by the block variables hv and gv. We simply want to add them together. Note that g is the (initially empty) hash object created by each_with_object, and returned by doit.

target_key = :id
target_value = '1'

qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
  #=> [{:id=>"1", :value=>"2"},{:id=>"1", :chips=>"4"},{:id=>"1", :value=>"5"}] 
qualified.empty?
  #=> false 
qualified.each_with_object({}) {|h,g|
  g.merge!(h) {|k,gv,hv| k == target_key ? gv : (gv.to_i + hv.to_i).to_s}}
  #=> {:id=>"1", :value=>"7", :chips=>"4"}

#2

The other common way to do this kind of calculation is to use Enumerable#flat_map, followed by Enumerable#group_by.

Code

def doit(arr, target_key, target_value)
  qualified = arr.select {|h|h.key?(target_key) && h[target_key]==target_value}
  return nil if qualified.empty?  
  qualified.flat_map(&:to_a)
           .group_by(&:first)
           .values.map { |a| a.first.first == target_key ? a.first :
             [a.first.first, a.reduce(0) {|tot,s| tot + s.last}]}.to_h
end

Explanation

This may look complex, but it's not so bad if you break it down into steps. Here's what's happening. (The calculation of qualified is the same as in #1.)

target_key = :id
target_value = '1'

c = qualified.flat_map(&:to_a)
  #=> [[:id,"1"],[:value,"2"],[:id,"1"],[:chips,"4"],[:id,"1"],[:value,"5"]] 
d = c.group_by(&:first)
  #=> {:id=>[[:id, "1"], [:id, "1"], [:id, "1"]],
  #    :value=>[[:value, "2"], [:value, "5"]],
  #    :chips=>[[:chips, "4"]]} 
e = d.values
  #=> [[[:id, "1"], [:id, "1"], [:id, "1"]],
  #    [[:value, "2"], [:value, "5"]],
  #    [[:chips, "4"]]] 
f = e.map { |a| a.first.first == target_key ? a.first :
  [a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }
  #=> [[:id, "1"], [:value, "7"], [:chips, "4"]]
f.to_h => {:id=>"1", :value=>"7", :chips=>"4"}
  #=> {:id=>"1", :value=>"7", :chips=>"4"}

Comment

You may wish to consider makin the values in the hashes integers and exclude the target_key/target_value pairs from qualified:

arr = [{:id  => 1, :value => 2}, {:id => 2, :value => 3},
       {:id  => 1, :chips => 4}, {:zd => 1, :value => 8},
       {:cat => 2, :value => 3}, {:id => 1, :value => 5}]

target_key = :id
target_value = 1

qualified = arr.select { |h| h.key?(target_key) && h[target_key]==target_value}
               .each { |h| h.delete(target_key) }
  #=> [{:value=>2}, {:chips=>4}, {:value=>5}] 
return nil if qualified.empty?  

Then either

qualified.each_with_object({}) {|h,g| g.merge!(h) { |k,gv,hv| gv + hv } }
  #=> {:value=>7, :chips=>4}

or

qualified.flat_map(&:to_a)
         .group_by(&:first)
         .values
         .map { |a| [a.first.first, a.reduce(0) {|tot,s| tot + s.last}] }.to_h
  #=> {:value=>7, :chips=>4}


来源:https://stackoverflow.com/questions/22701555/merge-hashes-based-on-particular-key-value-pair-in-ruby

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!