Coda Chang Coda Chang - 3 months ago 6
Ruby Question

Rails hash combine by their value

I have a

questions
list, and I need to separate them. The relationship is

Question_set has_many questions
BookVolume has_many questions
Subject has_many book_volumes
Publisher has_many subjects
Section has_many :questions


Now I only put
questions
and their relative model
id
,
name
into
hash
inside an
array
.

data = []
question_set.questions.each do |q|
data << {publisher: {id: q.publisher.id, name: q.publisher.name}, subject: {id: q.book_volume.subject.id, name: q.book_volume.subject.name}, volume: {id: q.book_volume_id, name: q.book_volume.name}, chapter: [{id: q.section_id, name: q.section.name}]}
end


Therefore, the
data
basically will be

>>data

[
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16155,
:name => "C1"
}
]
},
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16158,
:name => "C2"
}
]
}
]


However, I want the
chapter
to be combined if they got the same
publisher
,
subject
and
volume

So, in this case, it will be

>>data

[
{
:publisher => {
:id => 96,
:name => "P1"
},
:subject => {
:id => 233,
:name => "S1"
},
:volume => {
:id => 1136,
:name => "V1"
},
:chapter => [
{
:id => 16155,
:name => "C2"
},
{
:id => 16158,
:name => "C2"
}
]
}
]

Answer

Code

def group_em(data)
  data.group_by { |h| [h[:publisher], h[:subject], h[:volume]] }.
       map do |k,v|
         h = { publisher: k[0], subject: k[1], volume: k[2] }
         h.update(chapters: v.each_with_object([]) { |f,a|
           a << f[:chapter] }.flatten)
       end
end

Example

Let data equal the array of hashes (the first array above).

group_em(data)
  #=> [{:publisher=>{:id=>96, :name=>"P1"},
  #     :subject=>{:id=>233, :name=>"S1"},
  #     :volume=>{:id=>1136, :name=>"V1"},
  #     :chapters=>[{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
  #    } 
  #   ] 

Here data contains only two hashes and those hashes have the same values for the keys :publisher, :subject and :volume. This code allows the array to have any number of hashes, and will group them by an array of the values of those three keys, producing one hash for each of those groups. Moreover, the values of the key :chapters are arrays containing a single hash, but this code permits that array to contain multiple hashes. (If that array will always have exactly one hash, consider making the value of :chapters the hash itself rather than an array containing that hash.)

Explanation

See Enumerable#group_by and Hash#update (aka Hash#merge!).

The steps are as follows.

h = data.group_by { |h| [h[:publisher], h[:subject], h[:volume]] }
  #=> {
  #    [{:id=>96, :name=>"P1"},
  #     {:id=>233, :name=>"S1"},
  #     {:id=>1136, :name=>"V1"}
  #    ]=>[{:publisher=>{:id=>96, :name=>"P1"},
  #         :subject=>{:id=>233, :name=>"S1"},
  #         :volume=>{:id=>1136, :name=>"V1"},
  #         :chapter=>[{:id=>16155, :name=>"C1"}]
  #        },
  #        {:publisher=>{:id=>96, :name=>"P1"},
  #         :subject=>{:id=>233, :name=>"S1"},
  #         :volume=>{:id=>1136, :name=>"V1"},
  #         :chapter=>[{:id=>16158, :name=>"C2"}]
  #        }
  #       ]
  #   } 

The first key-value pair is passed to map's block and the block variables are assigned.

k,v = h.first
  #=> [[{:id=>96, :name=>"P1"}, {:id=>233, :name=>"S1"}, {:id=>1136, :name=>"V1"}],
  #   [{:publisher=>{:id=>96, :name=>"P1"}, :subject=>{:id=>233, :name=>"S1"},
  #     :volume=>{:id=>1136, :name=>"V1"}, :chapter=>[{:id=>16155, :name=>"C1"}]},
  #    {:publisher=>{:id=>96, :name=>"P1"}, :subject=>{:id=>233, :name=>"S1"},
  #     :volume=>{:id=>1136, :name=>"V1"}, :chapter=>[{:id=>16158, :name=>"C2"}]}]]
k #=> [{:id=>96, :name=>"P1"}, {:id=>233, :name=>"S1"}, {:id=>1136, :name=>"V1"}]
v #=> [{:publisher=>{:id=>96, :name=>"P1"},
  #     :subject=>{:id=>233, :name=>"S1"},
  #     :volume=>{:id=>1136, :name=>"V1"},
  #     :chapter=>[{:id=>16155, :name=>"C1"}]},
  #    {:publisher=>{:id=>96, :name=>"P1"},
  #     :subject=>{:id=>233, :name=>"S1"},
  #     :volume=>{:id=>1136, :name=>"V1"},
  #     :chapter=>[{:id=>16158, :name=>"C2"}]}] 

and the block calculation is performed.

h = { publisher: k[0], subject: k[1], volume: k[2] }
  #=> {:publisher=>{:id=>96, :name=>"P1"},
  #    :subject=>{:id=>233, :name=>"S1"},
  #    :volume=>{:id=>1136, :name=>"V1"}
  #   } 
a = v.each_with_object([]) { |f,a| a << f[:chapter] }
  #=> [[{:id=>16155, :name=>"C1"}], [{:id=>16158, :name=>"C2"}]] 
b = a.flatten
  #=> [{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
h.update(chapters: b)
  #=> {:publisher=>{:id=>96, :name=>"P1"},
  #    :subject=>{:id=>233, :name=>"S1"},
  #    :volume=>{:id=>1136, :name=>"V1"},
  #    :chapters=>[{:id=>16155, :name=>"C1"}, {:id=>16158, :name=>"C2"}]
  #   } 

Hash#merge could be used in place of Hash#update.