简体   繁体   中英

Convert array-of-hashes to a hash-of-hashes, indexed by an attribute of the hashes

I've got an array of hashes representing objects as a response to an API call. I need to pull data from some of the hashes, and one particular key serves as an id for the hash object. I would like to convert the array into a hash with the keys as the ids, and the values as the original hash with that id.

Here's what I'm talking about:

api_response = [
  { :id => 1, :foo => 'bar' },
  { :id => 2, :foo => 'another bar' },
  # ..
]

ideal_response = {
  1 => { :id => 1, :foo => 'bar' },
  2 => { :id => 2, :foo => 'another bar' },
  # ..
}

There are two ways I could think of doing this.

  1. Map the data to the ideal_response (below)
  2. Use api_response.find { |x| x[:id] == i } api_response.find { |x| x[:id] == i } for each record I need to access.
  3. A method I'm unaware of, possibly involving a way of using map to build a hash, natively.

My method of mapping:

keys = data.map { |x| x[:id] }
mapped = Hash[*keys.zip(data).flatten]

I can't help but feel like there is a more performant, tidier way of doing this. Option 2 is very performant when there are a very minimal number of records that need to be accessed. Mapping excels here, but it starts to break down when there are a lot of records in the response. Thankfully, I don't expect there to be more than 50-100 records, so mapping is sufficient.

Is there a smarter, tidier, or more performant way of doing this in Ruby?

Ruby <= 2.0

> Hash[api_response.map { |r| [r[:id], r] }]
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}} 

However, Hash::[] is pretty ugly and breaks the usual left-to-right OOP flow. That's why Facets proposed Enumerable#mash :

> require 'facets'
> api_response.mash { |r| [r[:id], r] }
#=> {1=>{:id=>1, :foo=>"bar"}, 2=>{:id=>2, :foo=>"another bar"}} 

This basic abstraction (convert enumerables to hashes) was asked to be included in Ruby long ago, alas, without luck .

Ruby >= 2.1

[UPDATE] Still no love for Enumerable#mash , but now we have Array#to_h . Not ideal -because we need an intermediate array- but better than nothing:

> object = api_response.map { |r| [r[:id], r] }.to_h

For this I'd probably just go:

ideal_response = api_response.each_with_object(Hash.new) { |o, h| h[o[:id]] = o }

Not super pretty with the multiple brackets in the block but it does the trick with just a single iteration of the api_response.

Something like:

ideal_response = api_response.group_by{|i| i[:id]} 
#=> {1=>[{:id=>1, :foo=>"bar"}], 2=>[{:id=>2, :foo=>"another bar"}]}

It uses Enumerable's group_by , which works on collections, returning matches for whatever key value you want. Because it expects to find multiple occurrences of matching key-value hits it appends them to arrays, so you end up with a hash of arrays of hashes. You could peel back the internal arrays if you wanted but could run a risk of overwriting content if two of your hash IDs collided. group_by avoids that with the inner array.

Accessing a particular element is easy:

ideal_response[1][0]       #=> {:id=>1, :foo=>"bar"}
ideal_response[1][0][:foo] #=> "bar"

The way you show at the end of the question is another valid way of doing it. Both are reasonably fast and elegant.

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