rj487
rj487

Reputation: 4634

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"
            }
        ]
    }
]

Upvotes: 1

Views: 79

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110675

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.

Upvotes: 2

lei liu
lei liu

Reputation: 2775

How about:

    data = {}

    question_set.questions.each do |q|
      key = "#{q.publisher.id}:#{q.book_volume.subject.id}:#{q.book_volume_id}"
      if data[key].present?
        data[key][:chapter] << {id: q.section_id, name: q.section.name}
      else
        data[key] = {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
    end

    result = data.values

use the combination of publisher'id, subject'id and volume'id as a unique key to combine your data.

Upvotes: 1

Related Questions