Severin
Severin

Reputation: 8588

Ruby - use mutated array for next iteration in loop

Given the following arrays:

arr1 = [
  { nested_test1: [1,2] },
  { nested_test2: [true, false] }
]
arr2 = [
  { test1: 'a', test2: 'b' }
]

I wanna loop over array arr2 and mutate it depending on the contents of arr1. The final result should look like this:

res_arr = [
  { test1: 'a', test2: 'b', nested_test1: 1, nested_test2: true },
  { test1: 'a', test2: 'b', nested_test1: 2, nested_test2: true },
  { test1: 'a', test2: 'b', nested_test1: 1, nested_test2: false },
  { test1: 'a', test2: 'b', nested_test1: 2, nested_test2: false },
]

So my initial setup looks like this:

res_arr = []
arr1.each do |hash1|
  arr2.each do |hash2|
    hash1.values.first.each do |value|
      hash2[hash1.keys.first] = value
      res_arr << hash2.clone
    end
  end
end

But this gives me the following:

res_arr = [
  {:test1=>"a", :test2=>"b", :nested_test1=>1},
  {:test1=>"a", :test2=>"b", :nested_test1=>2},
  {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>true},
  {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>false}
]

So what I wanna do then, is to use the mutated version for each iteration of arr2. How can this be achieved?

Upvotes: 0

Views: 138

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110685

Code

The following method permits its arguments to be arrays of arbitrary size.

def expand(arr1, arr2)
    arr2.product(
      arr1.map { |h| h.flatten.then { |k,v| v.map { |e| { k=>e } } } }.
           then { |first, *rest| first.product(*rest) }
    ).map { |first, (*rest)| first.merge(*rest.flatten) }
end

Example

arr1 = [{ nested_test1: [1,2] }, { nested_test2: [true, false] }]
arr2 = [{ test1: 'a', test2: 'b' }, { test3: 'c', test4: 'd' }]

expand(arr1, arr2)
  #=> [{:test1=>"a", :test2=>"b", :nested_test1=>1, :nested_test2=>true},
  #    {:test1=>"a", :test2=>"b", :nested_test1=>1, :nested_test2=>false},
  #    {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>true},
  #    {:test1=>"a", :test2=>"b", :nested_test1=>2, :nested_test2=>false},
  #    {:test3=>"c", :test4=>"d", :nested_test1=>1, :nested_test2=>true},
  #    {:test3=>"c", :test4=>"d", :nested_test1=>1, :nested_test2=>false},
  #    {:test3=>"c", :test4=>"d", :nested_test1=>2, :nested_test2=>true},
  #    {:test3=>"c", :test4=>"d", :nested_test1=>2, :nested_test2=>false}] 

Explanation

The following steps are performed for the example.

Noting that:

arr1.map(&:flatten)
  #=> [[:nested_test1, [1, 2]], [:nested_test2, [true, false]]] 

the first step is the following:

a = arr1.map { |h| h.flatten.then { |k,v| v.map { |e| { k=>e } } }
  #=> [[{:nested_test1=>1}, {:nested_test1=>2}],
  #    [{:nested_test2=>true}, {:nested_test2=>false}]]

See Hash#flatten and Object#then. The latter method made its debut in Ruby v2.6. It is an alias of Object#yield_self, which was new in v2.5.

Then:

b = a.then { |first, *rest| first.product(*rest) }
  #=> [[{:nested_test1=>1}, {:nested_test2=>true}],
  #    [{:nested_test1=>1}, {:nested_test2=>false}],
  #    [{:nested_test1=>2}, {:nested_test2=>true}],
  #    [{:nested_test1=>2}, {:nested_test2=>false}]]

See Array#product. Here and especially below I made heavy use of Array decomposition. See also this article.

Continuing,

c = arr2.product(b)
  #=> [[{:test1=>"a", :test2=>"b"}, [{:nested_test1=>1}, {:nested_test2=>true}]],
  #    [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>1}, {:nested_test2=>false}]],
  #    [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>2}, {:nested_test2=>true}]],
  #    [{:test1=>"a", :test2=>"b"}, [{:nested_test1=>2}, {:nested_test2=>false}]],
  #    [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>1}, {:nested_test2=>true}]],
  #    [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>1}, {:nested_test2=>false}]],
  #    [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>2}, {:nested_test2=>true}]],
  #    [{:test3=>"c", :test4=>"d"}, [{:nested_test1=>2}, {:nested_test2=>false}]]]

and lastly:

c.map { |first, (*rest)| first.merge(*rest.flatten) }
  #=> <as shown above>

See Hash#merge.

Upvotes: 0

Daniel Lucraft
Daniel Lucraft

Reputation: 7356

I'm not entirely clear what your goal here is. For instance, in arr2, can values of the hash be arrays? In arr1 can the hashes have more than one key value pair? Or what happens if arr2 has multiple hashes in it? Are they merged, or is the result expanded further somehow?

So it's hard to say exactly what the best course is.

Nonetheless, given the data you've given us and the result you say you want, you can get it like this using Array#product:

m = arr1.inject(&:merge)
m.values[0].product(m.values[1]).map {|a| Hash[m.keys.zip(a)].merge(arr2[0]) }

The intuition here is that I think you're trying to create all possible combinations of a couple of arrays, to generate all possible test cases? Depending on what you're trying to do, a more natural way to store the data and get the result might be:

m = { 
  test1: ['a'], 
  test2: ['b'],
  nested_test1: [1, 2],
  nested_test2: [true, false]
}

m.values.inject(&:product).map {|v| Hash[m.keys.zip(v.flatten)] }

Upvotes: 1

Related Questions