Combine two arrays of hashes

84 Views Asked by At

Looking for an elegant way to merge two arrays of hashes in a special way:

new_data = [{"name" => "a"}, {"name" => "b"}, {"name" => "c"}]
old_data = [{"name" => "a", "data" => "extra1"}, {"name" => "d", "data2" => "extra"}]
result = [{"name" => "a", "data" => "extra1"}, {"name" => "b"}, {"name" => "c"}]

The result must have all the the name hashes of new_data with just the extra data of old_data if the name key matches.

My first attempt was this, but it created an extra hash:

def combine(new_data, old_data)
  int = []
  new_data.each do |s|
    old_data.each do |e|
      (int << (s.merge e)) if e["name"] == s["name"]
    end
    int << s
  end
  int
end

# => [{"name"=>"a", "data"=>"extra1"}, {"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"}]
3

There are 3 best solutions below

1
On BEST ANSWER

A one-liner, but probably not too performant on large sets.

new_data.map{ |e| e.merge(old_data.detect{ |e2| e2['name'] == e['name'] } || {}) }
0
On

This is a little tricky, but you can tackle it with a transformation from new_data into a lookup hash, then iterate over old_data to merge in content:

new_data = [{"name" => "a"}, {"name" => "b"}, {"name" => "c"}]
old_data = [{"name" => "a", "data" => "extra1"}, {"name" => "d", "data2" => "extra"}]

# Transform into a lookup table using "name" as a key
lookup = Hash[new_data.collect { |v| [ v['name'], v ] }]

old_data.each do |data|
  # Match based on "name"
  found = lookup[data['name']]

  next unless (found)

  # If found, swap out the element with a merged version
  # so the original is preserved as-is, not mangled.
  lookup[data['name']] = found.merge(data)
end

lookup.values
# => [{"name"=>"a", "data"=>"extra1"}, {"name"=>"b"}, {"name"=>"c"}]
0
On

Assumptions

I have assumed:

  • in both new_data an old_data the values of "name" are unique;
  • new_data is an array of hashes containing one key-value pair; and
  • old_data is an array of hashes containing at least two key-value pairs.

Code

def extract_elements(new_data, old_data)
  (new_data+old_data).group_by { |h| h["name"] }.
                      values.
                      select {  |a| a.size > 1 || a.first.size == 1 }.
                      map(&:last)
end

Example

new_data = [{"name" => "a"}, {"name" => "b"}, {"name" => "c"}]
old_data = [{"name" => "a", "data" => "extra1"},
            {"name" => "d", "data2" => "extra"}]
extract_elements(new_data, old_data)
  #=> [{"name"=>"a", "data"=>"extra1"}, {"name"=>"b"}, {"name"=>"c"}] 

Explanation

For the example above:

a = (new_data+old_data)
  #=> [{"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"},
  #    {"name"=>"a", "data"=>"extra1"}, {"name"=>"d", "data2"=>"extra"}] 
b = a.group_by { |h| h["name"] }
  #=> {"a"=>[{"name"=>"a"}, {"name"=>"a", "data"=>"extra1"}],
  #    "b"=>[{"name"=>"b"}],
  #    "c"=>[{"name"=>"c"}],
  #    "d"=>[{"name"=>"d", "data2"=>"extra"}]} 
c = b.values
  #=> [[{"name"=>"a"}, {"name"=>"a", "data"=>"extra1"}],
  #    [{"name"=>"b"}],
  #    [{"name"=>"c"}],
  #    [{"name"=>"d", "data2"=>"extra"}]] 
d = c.select {  |a| a.size > 1 || a.first.size == 1 }
  #=> [[{"name"=>"a"}, {"name"=>"a", "data"=>"extra1"}],
  #    [{"name"=>"b"}],
  #    [{"name"=>"c"}]] 
e = d.map(&:last)
  #=> [{"name"=>"a", "data"=>"extra1"},
  #    {"name"=>"b"},
  #    {"name"=>"c"}] 

Proviso

This requires Ruby v1.9+, as it requires key-insertion order to be maintained for the hash b. For earlier versions, replace:

map(&:last)

with:

map { |h| h.max_by(&:size) }