gibihmruby
gibihmruby

Reputation: 41

Rails/Ruby: turning query result into multi-level hash

I have a query* that results in the following:

#<ActiveRecord::Relation [
    #<BookRank id: 2, book_id: 2, list_edition_id: 1, rank_world: 5, rank_europe: 1>,
    #<BookRank id: 3, book_id: 1, list_edition_id: 1, rank_world: 6, rank_europe: 2>,
    #<BookRank id: 8, book_id: 2, list_edition_id: 3, rank_world: 1, rank_europe: 1>,
    #<BookRank id: 9, book_id: 1, list_edition_id: 3, rank_world: 2, rank_europe: 2
]>

What I am trying to get is a hash like this:

{
    book_id => {
        list_edition_id => {
            "rank_world" => value,
            "rank_europe" => value
        }
    }
}

(The cherry on top would be to order the hash by the rank_world value for the lowest list_edition_id, but that may be too complex perhaps.)

ranks_relation.group_by(&:book_id) gives me a hash where the book_ids are keys, but then the ranks data is still in arrays:

{
    2 => [
            #<BookRank id: 2, book_id: 2, list_edition_id: 1, rank_world: 5, rank_europe: 1>,
            #<BookRank id: 8, book_id: 2, list_edition_id: 3, rank_world: 1, rank_europe: 1>
    ],
    1 => [
            #<BookRank id: 3, book_id: 1, list_edition_id: 1, rank_world: 6, rank_europe: 2>
            #<BookRank id: 9, book_id: 1, list_edition_id: 3, rank_world: 2, rank_europe: 2>
    ]
}

How should I proceed?

*EDIT: This is the model structure and query. Another user asked for it:

class Book < ActiveRecord::Base
  has_many :book_ranks, dependent: :destroy
end

class List < ActiveRecord::Base
  has_many :list_editions, dependent: :destroy
end

class ListEdition < ActiveRecord::Base
  belongs_to :list
  has_many :book_ranks, dependent: :destroy
end

class BookRank < ActiveRecord::Base
  belongs_to :book
  belongs_to :list_edition

  has_one :list, through: :list_edition
end

For the query, I already use two arrays with the relevant IDs for Book and ListEdition:

BookRank.where(:book_id => book_ids, :list_edition_id => list_edition_ids)

Upvotes: 2

Views: 230

Answers (3)

Rashid D R
Rashid D R

Reputation: 160

Try this

record = your_record

hash = {}

record.each do |record|
  hash[record.book_id] ||= {}
  hash[record.book_id][record.list_edition_id] = {
    'rank_world' => record.rank_world,
    'rank_europe' => record.rank_europe
  }
end
# hash will then be {2=>{1=>{"rank_world"=>5, "rank_europe"=>1}, 3=>{"rank_world"=>1, "rank_europe"=>1}}, 1=>{1=>{"rank_world"=>6, "rank_europe"=>2}, 3=>{"rank_world"=>2, "rank_europe"=>2}}}

This will iterate through record only once.

Upvotes: 2

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121010

ranks_relation.
  group_by(&:book_id).
  map do |id, books|
    [id, books.map do |book|
           [
             book.list_edition_id,
             {
               "rank_world" => book.rank_world,
               "rank_europe" => book.rank_europe
             }
           ]
         end.sort_by { |_, hash| hash["rank_world"] }.to_h
    ]
  end.to_h

Upvotes: 0

Jay Schneider
Jay Schneider

Reputation: 325

Hey @gibihmruby (nice name btw),

so since you asked in the comments for a more specific description of my ugly approach using just group_by and friends, here is my proposal:

rel.group_by(&:book_id).map do |k, v| 
  [k, v.group_by(&:list_edition_id)]
end.to_h

would yield a structure like

{2=>
  {1=>
    [#<struct BookRank
      id=2,
      book_id=2,
      list_edition_id=1,
      rank_world=5,
      rank_europe=1>],
   3=>
    [#<struct BookRank
      id=8,
      book_id=2,
      list_edition_id=3,
      rank_world=1,
      rank_europe=1>]},
 1=>
  {1=>
    [#<struct BookRank
      id=3,
      book_id=1,
      list_edition_id=1,
      rank_world=6,
      rank_europe=2>],
   3=>
    [#<struct BookRank
      id=9,
      book_id=1,
      list_edition_id=3,
      rank_world=2,
      rank_europe=2>]}}

You then would have to map the most inner object to the attributes you want. If you are sure that the combination of book_id and list_edition_id is unique, you can get rid of the array wrapping and then map to the required attributes. You can use slice for ActiveRecord objects. The mapping would then be

rel.group_by(&:book_id).map do |book_id, grouped_by_book_id| 
  [
    book_id, 
    grouped_by_book_id.group_by(&:list_edition_id).map do |list_ed_id, grouped|
      [list_ed_id, grouped.first.slice(:rank_world, :rank_europe)]
    end.to_h
  ]
end.to_h

Since I didn't create a model but simply used structs (as you can see in my example above), I didn't really test the last bit by myself. But it should work like this, please comment if you found a mistake or have more questions. I still hope someone comes up with a better solution since I was looking for one myself way too often now.

Cheers :)

edit: minor corrections

Upvotes: 1

Related Questions