Arthur Huynh
Arthur Huynh

Reputation: 35

How to merge nested arrays of varying lengths, given they share an element in Ruby?

I have nested arrays made up of strings and ints, of which I want to consolidate according to the first element of each array. Let's say these are item ids. The nested arrays are of varying length but as long as the first element of each nested array are the same, I want to merge them together.

My data might look like this:

arr = [['00', 'apples', nil, nil, 8],
['00', 'apples', nil, 2],
['00', 'apples', nil, nil, nil, 3],
['01', 'bananas', 2],
['01', 'bananas', nil, nil, 3],
['01', 'bananas', nil, 5]]

So I would like to condense these arrays so they look like:

arr = [['00', 'apples', nil, 2, 8, 3],
['01', 'bananas', 2, 5, 3]]

There won't be an instance where the second element (in this case apples or bananas) will be different for the given first element (00 or 01). So as long as the first element is the same between any two of the nested arrays, I want to merge them together.

Also, the data guarantees no collisions between the numbers for arrays sharing the same first element. However, the numbers should maintain their index positions post-merge. This is crucial.

I'm not quite sure how to approach this. I was thinking of creating an empty array and then iterating through the nested arrays checking to see if the first element is contained in the new array. If not, push that array to the new array. If so, merge them. The code I have doesn't work (I get an empty array)...

    new_array = Array.new
    old_array.each do |o|
      new_array.each do |n|
        if o[0] == n[0]
          n = o | n
        else
          n.push(o)
        end
      end
    end

I looked into .reduce(:|) but it gives me undesired results:

arr = [["00", "apples", nil, nil, nil, 8],
 ["00", "apples", nil, nil, nil, nil, 1],
 ["00", "apples", nil, nil, 1]]
arr.reduce(:|)
=> ["00", "apples", nil, 8, 1]

I was expecting: => ["00", "apples", nil, nil, 1, 8, 1]

Upvotes: 0

Views: 119

Answers (2)

sugaryourcoffee
sugaryourcoffee

Reputation: 879

You could use a hash and convert it to an array

new_hash = {}
old_array.each do |o|
  new_hash[o[0]] ||= []
  o.each_with_index do |n, i|
    new_hash[o[0]][i] ||= n
  end
end

puts new_hash.inspect
puts new_hash.map { |k,v| v }.inspect

This will result in

{"00"=>["00", "apples", nil, 2, 8, 3], "01"=>["01", "bananas", 2, 5, 3]}
[["00", "apples", nil, 2, 8, 3], ["01", "bananas", 2, 5, 3]]

Upvotes: 0

Cary Swoveland
Cary Swoveland

Reputation: 110725

There are several ways to do this. One is to use Enumerable#group_by. Others may mention methods which merge hashes.

arr = [
  ['00', 'apples', nil, nil, 8],
  ['00', 'apples', nil, 2],
  ['00', 'apples', nil, nil, nil, 3],
  ['01', 'bananas', 2],
  ['01', 'bananas', nil, nil, 3],
  ['01', 'bananas', nil, 5]
]

arr.group_by(&:first).values.map { |a| a.reduce(:|) }
  #=> [["00", "apples", nil, 8, 2, 3], ["01", "bananas", 2, nil, 3, 5]]

These are the steps:

b = arr.group_by(&:first)
  #=> {"00"=>[["00", "apples", nil, nil, 8], ["00", "apples", nil, 2],
  #           ["00", "apples", nil, nil, nil, 3]],
  #    "01"=>[["01", "bananas", 2], ["01", "bananas", nil, nil, 3],
  #           ["01", "bananas", nil, 5]]}
c = b.values
  #=> [[["00", "apples", nil, nil, 8], ["00", "apples", nil, 2],
  #     ["00", "apples", nil, nil, nil, 3]],
  #    [["01", "bananas", 2], ["01", "bananas", nil, nil, 3],
  #     ["01", "bananas", nil, 5]]]
c.map { |a| a.reduce(:|) }     
  #=> [["00", "apples", nil, 8, 2, 3], ["01", "bananas", 2, nil, 3, 5]]

The first element of c is passed to map's block and assigned to the block variable:

a = [["00", "apples", nil, nil, 8], ["00", "apples", nil, 2],
     ["00", "apples", nil, nil, nil, 3]]

a.reduce(:|) gives the same result as a.reduce { |d,e| d | e }, which is merely the union of the three elements (arrays) of a:

(["00", "apples", nil, nil, 8] | ["00", "apples", nil, 2]) |
["00", "apples", nil, nil, nil, 3]]
  #=> ["00", "apples", nil, 8, 2] | ["00", "apples", nil, nil, nil, 3] 
  #=> ["00", "apples", nil, 8, 2, 3]

A similar calculation is performed for the second element of a passed to map's block.

Upvotes: 2

Related Questions