Ger Cas
Ger Cas

Reputation: 2298

Merge hashes of arrays based on similar positions with Ruby

I have the following two hashes with arrays as values.

a = {
  "Us" => [["1", ["1", "2"]], ["2", ["1"]]],
  "Pa" => [["1", ["1", "3", "5"]], ["4", ["7"]]]
}
b = {
  "Us" => [["1", ["F", "O"]], ["2", ["N"]]],
  "Pa" => [["1", ["S", "D", "H"]], ["4", ["K"]]]
}

I'm trying to merge the hashes to get a final has like this:

c = {
  "Us" => [["1", ["1|F", "2|O"]], ["2", ["1|N"]]],
  "Pa" => [["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]
}

I found the following code with merge, and tried to apply it to my issue, but I got an error:

a.merge(b) {|key, a_val, b_val| a_val.merge b_val }
# >> NoMethodError: undefined method `merge' for [["1", ["1", "2"]], ["2", ["1"]]]:Array

I even got an error with a + b:

a + b
# >> NoMethodError: undefined method `+' for #<Hash:0x0000060078e460>

<<<< UPDATE >>>>

Thanks both Cary and tadman. Outside the original question I show the input file I have and the output I´m tryng to obtain. I show in order for you get an idea why I generated 2 hashes in that way. In the output I create blocks where fathers are unique values of column 1, below children (unique values in column 2 related with col 1). Column 3 are subchildren that belong to a value in col2 and column 4 are text contents related with col3.

Probably hash "c" is easier to generate from the beginning.

This is my input file

Main,Stage1,Stage2,Description
Us,1,1,F
Us,1,2,O
Us,2,1,N
Pa,1,1,S
Pa,1,3,D
Pa,1,5,H
Pa,4,7,K

This is the output I almost get.

Main..Stage1..Stage2..Description       
Us      
......1
..............1.......F
..............2.......O
......2 
..............1.......N
Pa
......1
..............1.......S
..............3.......D
..............5.......H
......4
..............7.......K

Then I was able to create this code, but like tadman says, I need to reorder the way I get this to make easier the things, since I use 4 hashes. After I create hash "a" and "b" I was stuck, since I needed a unique hash to iterate and be able to print in the output structure shown above.

My code before post the question

X = Hash.new{|hsh,key| hsh[key] = [] }
Y = Hash.new{|hsh,key| hsh[key] = [] }
a = Hash.new{|hsh,key| hsh[key] = [] }
b = Hash.new{|hsh,key| hsh[key] = [] }

File.foreach('file.txt').with_index do
    |line, line_num|

    if line_num > 0
        r = line.split(",")

        X[r[0] + "°" + r[1]].push r[2]
        Y[r[0] + "°" + r[1]].push r[3].strip
    end
end

X.each{ |k,v|
    lbs = k.split("°")
    a[lbs[0]].push [ lbs[1], v] #Here I generate hash a
}

Y.each{ |k,v|
    lbs = k.split("°")
    b[lbs[0]].push [ lbs[1], v] #Here I generate hash b
}

Upvotes: 3

Views: 107

Answers (3)

L. Jacob
L. Jacob

Reputation: 141

In this solution we'll keep the original structure.

I've followed your first try but instead of:

a.merge(b) {|key, a_val, b_val| a_val.merge b_val }

Think about use a new custom merge function like:

c = a.merge(b) {|key, a_val, b_val| myMergeArray(a_val, b_val) }

Then the new merge function is a simple recursive one:

def myMergeArray(a,b,sep = '|')
 c = a
 c.each_with_index { |e, i|
    if c[i].is_a? Array 
        c[i] = myMergeArray(c[i], b[i], sep)
    else
        c[i] = c[i] + sep + b[i] if c[i] != b[i]
    end
        }
 return c
end

I've assumed that in case of equal elements, just save one, so e.g. "Y" and "Y" yield just "Y" instead of "Y|Y"

Cheers!

Upvotes: 1

Cary Swoveland
Cary Swoveland

Reputation: 110725

I suggest you first convert the values of one of the hashes to hashes, as I will explain. Suppose we create a new b.

newbie = b.transform_values(&:to_h)
  #=> {"Us"=>{"1"=>["F", "O"], "2"=>["N"]},
  #    "Pa"=>{"1"=>["S", "D", "H"], "4"=>["K"]}}

We can now use a and newbie to produce the desired return value.

a.each_with_object({}) do |(k,v),h|
  h[k] = v.map do |first, arr|
    [first, arr.zip(newbie[k][first]).map { |pair| pair.join('|') }]
  end
end
  #=> {"Us"=>[["1", ["1|F", "2|O"]], ["2", ["1|N"]]],
  #    "Pa"=>[["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]}

If a can be mutated it's slightly easier.

a.each do |k,v|
  v.map! do |first, arr|
    [first, arr.zip(newbie[k][first]).map { |pair| pair.join('|') }]
  end
end

The method Hash#trasform_values made its debut in Ruby v2.4. To support older versions, one compute newbie as follows.

newbie = b.each_with_object({}) {|(k,v),h| h[k] = v.to_h }

Upvotes: 2

tadman
tadman

Reputation: 211690

What you have here is going to require a bit of work to solve because of all the complicated nesting. This would be a lot easier if you did some work to reorder how that data is stored.

Yet you can do this:

a={"Us"=>[["1", ["1", "2"]], ["2", ["1"]]], "Pa"=>[["1", ["1", "3", "5"]], ["4", ["7"]]]}
b={"Us"=>[["1", ["F", "O"]], ["2", ["N"]]], "Pa"=>[["1", ["S", "D", "H"]], ["4", ["K"]]]}

c = a.keys.map do |k|
  ah = a[k].to_h
  bh = b[k].to_h

  [
    k,
    ah.keys.map do |ka|
      [
        ka,
        ah[ka].zip(bh[ka]).map do |pair|
          pair.join('|')
        end
      ]
    end
  ]
end.to_h

# => {"Us"=>[["1", ["1|F", "2|O"]], ["2", ["1|N"]]], "Pa"=>[["1", ["1|S", "3|D", "5|H"]], ["4", ["7|K"]]]}

The key here is rigorous use of map to transform each layer and zip to "zipper" two arrays together into pairs that can then be combined with join into the desired string target. Cast back to a Hash with to_h at the end and you get what you want.

There's an intermediate conversion for each subset to a hash to handle out-of-order situations where one might specify the apparent "keys" in a different sequence.

What you'll want to do is wrap this up in a method with a descriptive name:

def hash_compactor(a,b)
  # ... (code) ...
end

That'll help keep it modular. Normally I try and create solutions that handle N arguments by defining it as:

def hash_compactor(*input)
  # ...
end

Where input is then an array of various sets in the form you've given. The resulting code is surprisingly a lot more complicated.

Note this makes a lot of assumptions about the input being perfectly matched and will explode if that's not the case.

Upvotes: 3

Related Questions