简体   繁体   中英

Merge hashes based on particular key/value pair in ruby

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.

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}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM