1

So, I have a hash with arrays, like this one:

{"name": ["John","Jane","Chris","Mary"], "surname": ["Doe","Doe","Smith","Martins"]}

I want to merge them into an array of hashes, combining the corresponding elements.

The results should be like that:

[{"name"=>"John", "surname"=>"Doe"}, {"name"=>"Jane", "surname"=>"Doe"}, {"name"=>"Chris", "surname"=>"Smith"}, {"name"=>"Mary", "surname"=>"Martins"}] 

Any idea how to do that efficiently?


Please, note that the real-world use scenario could contain a variable number of hash keys.

Dr.Kameleon
  • 22,532
  • 20
  • 115
  • 223

5 Answers5

2

Try this

h[:name].zip(h[:surname]).map do |name, surname|
  { 'name' => name, 'surname' => surname }
end
Ursus
  • 29,643
  • 3
  • 33
  • 50
1

I suggest writing the code to permit arbitrary numbers of attributes. It's no more difficult than assuming there are two (:name and :surname), yet it provides greater flexibility, accommodating, for example, future changes to the number or naming of attributes:

def squish(h)
  keys = h.keys.map(&:to_s)
  h.values.transpose.map { |a| keys.zip(a).to_h }
end

h = { name:    ["John", "Jane", "Chris"],
      surname: ["Doe", "Doe", "Smith"],
      age:     [22, 34, 96]
    }    

squish(h)
  #=> [{"name"=>"John", "surname"=>"Doe", "age"=>22},
  #    {"name"=>"Jane", "surname"=>"Doe", "age"=>34},
  #    {"name"=>"Chris", "surname"=>"Smith", "age"=>96}] 

The steps for the example above are as follows:

b = h.keys
  #=> [:name, :surname, :age] 
keys = b.map(&:to_s)
  #=> ["name", "surname", "age"] 
c = h.values
  #=> [["John", "Jane", "Chris"], ["Doe", "Doe", "Smith"], [22, 34, 96]] 
d = c.transpose
  #=> [["John", "Doe", 22], ["Jane", "Doe", 34], ["Chris", "Smith", 96]] 
d.map { |a| keys.zip(a).to_h }
  #=> [{"name"=>"John", "surname"=>"Doe", "age"=>22},
  #    {"name"=>"Jane", "surname"=>"Doe", "age"=>34},
  #    {"name"=>"Chris", "surname"=>"Smith", "age"=>96}] 

In the last step the first value of b is passed to map's block and the block variable is assigned its value.

a = d.first
  #=> ["John", "Doe", 22] 
e = keys.zip(a)
  #=> [["name", "John"], ["surname", "Doe"], ["age", 22]] 
e.to_h
  #=> {"name"=>"John", "surname"=>"Doe", "age"=>22} 

The remaining calculations are similar.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
1
[h[:name], h[:surname]].transpose.map do |name, surname|
  { 'name' => name, 'surname' => surname }
end
Marian13
  • 7,740
  • 2
  • 47
  • 51
  • Please don't post only code as an answer, but also provide an explanation what your code does and how it solves the problem of the question. Answers with an explanation are usually of higher quality, and are more like to attract upvotes. – Mark Rotteveel Apr 12 '20 at 07:28
1

If your dataset is really big, you can consider using Enumerator::Lazy.

This way Ruby will not create intermediate arrays during calculations.

This is how @Ursus answer can be improved:

h[:name]
  .lazy
  .zip(h[:surname])
  .map { |name, surname| { 'name' => name, 'surname' => surname } }
  .to_a
Marian13
  • 7,740
  • 2
  • 47
  • 51
1

Other option for the case where:

[..] the real-world use scenario could contain a variable number of hash keys

h = {
      'name': ['John','Jane','Chris','Mary'],
      'surname': ['Doe','Doe','Smith','Martins'],
      'whathever': [1, 2, 3, 4, 5]
    }

You could use Object#then with a splat operator in a one liner:

h.values.then { |a, *b| a.zip *b }.map { |e| (h.keys.zip e).to_h }

#=> [{:name=>"John", :surname=>"Doe", :whathever=>1}, {:name=>"Jane", :surname=>"Doe", :whathever=>2}, {:name=>"Chris", :surname=>"Smith", :whathever=>3}, {:name=>"Mary", :surname=>"Martins", :whathever=>4}]

The first part, works this way:

h.values.then { |a, *b| a.zip *b }
#=> [["John", "Doe", 1], ["Jane", "Doe", 2], ["Chris", "Smith", 3], ["Mary", "Martins", 4]]

The last part just maps the elements zipping each with the original keys then calling Array#to_h to convert to hash.

Here I removed the call .to_h to show the intermediate result:

h.values.then { |a, *b| a.zip *b }.map { |e| h.keys.zip e }
#=> [[[:name, "John"], [:surname, "Doe"], [:whathever, 1]], [[:name, "Jane"], [:surname, "Doe"], [:whathever, 2]], [[:name, "Chris"], [:surname, "Smith"], [:whathever, 3]], [[:name, "Mary"], [:surname, "Martins"], [:whathever, 4]]]
iGian
  • 11,023
  • 3
  • 21
  • 36