4

In Ruby, how do I go from this:

[
  1, 
  ["green", "blue", "black"], 
  [ ["1", "2"], ["3"], ["4", "5"] ]
]

to this?

[
  [1, "green", "1"],
  [1, "green", "2"],
  [1, "blue", "3"],
  [1, "black", "4"],
  [1, "black", "5"],
]

I tried .zip but with no luck. Any help is greatly appreciated, and of course I'm looking for a performant solution.

1
  • 1
    When you given an example it's helpful to assign a variable to each input object (e.g., arr = [1, ["green", "blue", "black"], [ ["1", "2"], ["3"], ["4", "5"] ]]. That way, readers can refer to the variables in answers and comments without having define them, and all readers will use the same variable names. Commented Sep 16, 2017 at 19:10

3 Answers 3

3

The pattern is not completely clear to me, but this gets the expected output for the example you provided:

data[1].
  zip(data[2]).
  flat_map { |x, ys| [x].product(ys) }.
  map { |zs| [data[0], *zs] }
#=> [[1, "green", "1"], [1, "green", "2"], [1, "blue", "3"], 
#    [1, "black", "4"], [1, "black", "5"]]
Sign up to request clarification or add additional context in comments.

Comments

2

We are given

arr = [1, ["green", "blue", "black"], [ ["1", "2"], ["3"], ["4", "5"] ]]

Here are a couple of ways to obtain the desired result.

#1

arr[1].flat_map.with_index { |color,i| [arr[0]].product([color], arr[2][i]) }
  #=> [[1, "green", "1"], [1, "green", "2"], [1, "blue", "3"],
  #    [1, "black", "4"], [1, "black", "5"]]

The steps are as follows.

enum0 = arr[1].flat_map
  #=> #<Enumerator: ["green", "blue", "black"]:flat_map>
enum1 = enum0.with_index
  #=> #<Enumerator: #<Enumerator: ["green", "blue", "black"]:flat_map>:with_index>

enum1 can be thought of as a compound enumerator. We can see the values that will be generated by enum1 and passed to the block by converting it to an array.

enum1.to_a
  #=> [["green", 0], ["blue", 1], ["black", 2]]

The first value is generated and passed to the block, the block variables are assigned and the block calculation is performed.

color, i = enum1.next
  #=> ["green", 0]
color
  #=> "green"
i #=> 0
[arr[0]].product([color], arr[2][i])
  #=> [1].product(["green"], )
  #=> [[1, "green", "1"], [1, "green", "2"]]

The calculations are similar for the two remaining elements generated by enum1.

An alternative to this is to dup arr[2] and shift elements of the dup:

a2 = arr[2].dup
arr[1].flat_map { |color,i| [arr[0]].product([color], a2.shift) }

#2

arr[1].zip(arr[2]).flat_map { |color, a| [arr[0]].product([color], a) }
  #=> [[1, "green", "1"], [1, "green", "2"], [1, "blue", "3"],
  #    [1, "black", "4"], [1, "black", "5"]]

The steps are as follows.

b = arr[1].zip(arr[2])
  #=> [["green", ["1", "2"]], ["blue", ["3"]], ["black", ["4", "5"]]]

b[0] is passed to flat_map and the block variables are assigned and the block calculation is performed.

color, a = b[0]
  #=> ["green", ["1", "2"]]
color
  #=> "green"
a #=> ["1", "2"]
[arr[0]].product([color], a)
  #=> [["1"]].product(["green"], ["1", "2"])
  #=>  [[1, "green", "1"], [1, "green", "2"]]

After the remaining elements of b are passed to map the desired array is returned by Enumerable#flat_map.

1 Comment

Thanks, @tokland. As a flat_map believer, I should have seen that. I edited.
1

I needed a more generic and flexible solution as compared to the ones proposed (my bad, I should have been more clear about the requirements), so I came up with the following:

class Array
  def transpose_rows
    arys = self.select{|el| el.is_a?(Array)}
    if arys.size == 0
      [self]
    else 
      result = []
      (arys.map(&:size).max || 1).times.map{ |i|
        self.map { |r| 
          r.is_a?(Array) ? r[i] : r
        }.transpose_rows.map{|r| result << r}
      }
      result
    end
  end
end

The initial spec is that every element in the array is either a value or another array of varying depth. Each subarray "explodes" the values of the subarrays of depth-1 into an arbitrary number of "sub-values". The result should be a set of rows listing all combinations deriving from the original array.

The other solutions proposed do work for the original array I posted, which was just an example, while this one works for more complex scenarios such as the following:

[
  2, 
  [3,4,5], 
  6, 
  [7,8,9], 
  [ [11,22], [33], [44,55] ], 
  [0, 1, 2],
  [ [66], [77], [nil,99] ],
  4
].transpose_rows

# => [
#   [2, 3, 6, 7, 11, 0, 66, 4], 
#   [2, 3, 6, 7, 22, 0, nil, 4], 
#   [2, 4, 6, 8, 33, 1, 77, 4], 
#   [2, 5, 6, 9, 44, 2, nil, 4], 
#   [2, 5, 6, 9, 55, 2, 99, 4]
# ]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.