Richard Lyon February 2016

Hash mapping riddle

In Ruby, how do I convert this:

{"1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
 "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"},
 "3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"},
 "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}}

into this:

{"album1"=>
  {"1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
   "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"}},
 "album2"=>
  {"3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"},
   "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}}}

in the most efficient way.

The first is the format that iTunes stores track information. The last is the format I'd need to process tracks at the level of 'album'. I've been staring at this all day and, not being very good at Ruby, have conceded defeat. Thank you for the tutorial on hash kung-foo.


EDIT

While I was waiting for the moderator to decide if this was OK, I got a solution:

album_tracks = {}
titles = []
tracks_hash.each do |album_id, album_hash|
  titles << album_hash["album"] if !titles.include? album_hash["album"]
end

titles.each do |title|
  tracks = {}
  tracks_hash.each do |album_id, album_hash|
    tracks[album_id] = album_hash if title == album_hash["album"]
  end
  albums_hash[title] = tracks
end

I'm guessing there is a more efficient strategy involving some sort of mapping that doesn't require passing over the entire hash twice?

Answers


meagar February 2016

Your output can be achieved through a pretty straight-forward call to group_by, followed by a few transforms to turn the results back into hashes:

albums = {"1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
          "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"},
          "3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"},
          "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}}

albums.group_by { |k,v| v['album'] }.map { |k,v| [k, v.to_h] }.to_h

# => {
#  "album1"=> {
#    "1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
#    "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"}
#   },
#  "album2"=>{
#    "3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"},
#    "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}
#  }
#}

The key is understanding which methods are available on Enumerable for translating one structure into another (ie group_by and map) and then knowing that Ruby lets you freely transform arrays to hashes and vice versa.

The first, call, albums.group_by { |k,v| v['album'] }, produces the correct outer Hash structure, but the values have the form [[key1, value1], [key2, value2], ...]. Ruby will let you turn that same structure back into a {key1: value1, key2: value2} hash using to_h.


Nabeel Amjad February 2016

This should do the trick.

album_tracks = tracks_hash.each_with_object({}) do |(album_id, album_hash), album_tracks|
  album_tracks[album_hash['album']] ||= {}
  album_tracks[album_hash['album']][album_id] = album_hash
end


Cary Swoveland February 2016

One way is to use the form of Hash#update (aka merge!) that employs a block to determine the values of keys that are present in both hashes being merged.

h = { "1"=>{ "id"=>1, "album"=>"album1", "track"=>"track1" },
      "2"=>{ "id"=>2, "album"=>"album1", "track"=>"track2" },
      "3"=>{ "id"=>3, "album"=>"album2", "track"=>"track1" },
      "4"=>{ "id"=>4, "album"=>"album2", "track"=>"track2" } }

h.each_with_object({}) do |(k,v),g|
  g.update(v["album"]=>{ k=>v}) { |_,o,n| o.update(n) }
end
  #=> {"album1"=>{"1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
  #               "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"}},
  #    "album2"=>{"3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"},
  #               "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}}}   

Note that update's argument,

v["album"]=>{ k=>v}

is shorthand for the hash

{ v["album"]=>{ k=>v} }

The steps:

enum = h.each_with_object({})
  #=> #<Enumerator: {"1"=>{"id"=>1, "album"=>"album1", "track"=>"track1"},
  #                  "2"=>{"id"=>2, "album"=>"album1", "track"=>"track2"},
  #                  "3"=>{"id"=>3, "album"=>"album2", "track"=>"track1"}, 
  #                  "4"=>{"id"=>4, "album"=>"album2", "track"=>"track2"}}:
  #                  each_with_object({})> 

The first element of enum is passed to the block and the block variables are assigned using decomposition:

 (k,v),g = enum.next
   #=> [["1", {"id"=>1, "album"=>"album1", "track"=>"track1"}], {}] 
 k #=> "1" 
 v #=> {"id"=>1, "album"=>"a 

Post Status

Asked in February 2016
Viewed 3,598 times
Voted 4
Answered 3 times

Search




Leave an answer