Question

I would like to expand a Ruby array (which may contain some sub-arrays) into another array of arrays just like in these examples:

Example 1: [:foo, :bar]

[
  [:foo, :bar]
]

Example 2: [:foo, :bar, [:ki, :ku]]

[
  [:foo, :bar, :ki],
  [:foo, :bar, :ku]
]

Example 3: [:foo, :bar, :baz, [:a, :i, :u, :e, :o], :qux]

[
  [:foo, :bar, :baz, :a, :qux],
  [:foo, :bar, :baz, :i, :qux],
  [:foo, :bar, :baz, :u, :qux],
  [:foo, :bar, :baz, :e, :qux],
  [:foo, :bar, :baz, :o, :qux]
]

Example 4: [:foo, :bar, :baz, [:a, :i, :u, :e, :o], [1, 2], :qux]

[
  [:foo, :bar, :baz, :a, 1, :qux],
  [:foo, :bar, :baz, :i, 1, :qux],
  [:foo, :bar, :baz, :u, 1, :qux],
  [:foo, :bar, :baz, :e, 1, :qux],
  [:foo, :bar, :baz, :o, 1, :qux],
  [:foo, :bar, :baz, :a, 2, :qux],
  [:foo, :bar, :baz, :i, 2, :qux],
  [:foo, :bar, :baz, :u, 2, :qux],
  [:foo, :bar, :baz, :e, 2, :qux],
  [:foo, :bar, :baz, :o, 2, :qux]
]

Example 5: [:foo, [[], :c], :bar]

[
  [:foo, [], :bar],
  [:foo, :c, :bar]
]

Example 6: [:foo, [[:a, :b], :c], :bar]

[
  [:foo, [:a, :b], :bar],
  [:foo, :c, :bar]
]

Note: Only sub-arrays should be expanded. That's why, in example 5 & 6, sub-sub-arrays are not expanded.

Many thanks for any suggestions or piece of code.

Was it helpful?

Solution

I used the product idea to reach this function:

def trans(a)
  b = a.map{|e| [e].flatten(1)}
  b.first.product(*b.slice(1..-1))
end

For example, this code:

puts trans([:foo, :bar]).inspect
puts trans([:foo, :bar, :baz, [:a, :i, :u, :e, :o], [1, 2], :qux]).inspect
puts trans([:foo, [[], :c], :bar]).inspect
puts trans([:foo, [[:a, :b], :c], :bar]).inspect

Gives this:

[[:foo, :bar]]
[[:foo, :bar, :baz, :a, 1, :qux],
 [:foo, :bar, :baz, :a, 2, :qux],
 [:foo, :bar, :baz, :i, 1, :qux],
 [:foo, :bar, :baz, :i, 2, :qux],
 [:foo, :bar, :baz, :u, 1, :qux],
 [:foo, :bar, :baz, :u, 2, :qux],
 [:foo, :bar, :baz, :e, 1, :qux],
 [:foo, :bar, :baz, :e, 2, :qux],
 [:foo, :bar, :baz, :o, 1, :qux],
 [:foo, :bar, :baz, :o, 2, :qux]]
[[:foo, [], :bar],
 [:foo, :c, :bar]]
[[:foo, [:a, :b], :bar],
 [:foo, :c, :bar]]

EDIT: An explanation for the code above.

The general idea is that we want a product of all elements in the array. If you look at the documentation of Array#product, you'll see that it does what you want -- we just have to call it appropriately.

First, product operates on arrays, so we have to make sure that all items in our original array are array themselves. This is the task of the first line of the function:

b = a.map{|e| [e].flatten(1)}

We are converting all elements in the array by using map. The conversion makes an array with the element e inside, and then flattens this new array. Either the original element was an array or it wasn't; if it wasn't an array, [e].flatten(1) will do nothing and will return [e]; if it was an array, [e] will evaluate to [[x]], which will then be flattened to [x]. The 1 tells flatten to go only 1 level deep.

Then all we have to do is call product on the first element passing as arguments the remaining elements of the modified array:

b.first.product(*b.slice(1..-1))

Here, b.slice(1..-1) means: take elements from b, beginning from the 2nd all the way to the last. Finally, the asterisk indicates that we don't want to pass the array as an argument, but instead the elements of the array.

OTHER TIPS

It seems you wish to have a Cartesian product of elements of the array in question. This code should work for you:

array = [:foo, :bar, :baz, [:a, :i, :u, :e, :o], [1, 2], :qux]
array.inject([[]]) do |product,element|
  result = []
  if element.is_a?(Array)
    product.each do |tuple|
      element.each do |last|
        result << tuple + [last]
      end
    end
  else
    product.each do |tuple|
      result << tuple + [element]
    end
  end
  result
end

You can simplify it a little bit by moving the conditional to the loop, but this will make it less efficient.

Considering you're using examples rather than something explicit I'd suggest having a trawl through the Array method's documentation. For starters have a look at the following methods:

.combination.to_a
.shift
.transpose
.flatten
.zip
.take

How you'd implement depends on whether you know what you're transforming in each case or are you trying to build something generic (i.e. extend the Array class. For each I'd manipulate the input array into a target array. Example 1 is easy:

input = [:foo,:bar]
target = Array.new
target << input        => [[:foo,:bar]]

For the rest of the examples there is multiple ways of getting there depending on what you're trying to do, and how you want ruby to know what to do when presented with the input. In example 2 the first is to directly write:

input = [:foo, :bar, [:ki, :ku]]
target = Array.new

target << [input[0], input[1], input[2][0]]
target << [input[0], input[1], input[2][1]]

Or playing with array methods:

target = input.pop
target = [input, input].zip(target).flatten
target = [target[0..(target.size/2)-1], target[target.size/2..-1]]

Or if you didn't know which the part of the array which contained a sub-array, you could detect it:

input.each do |i|
  if i.class == Array
    holding = i
  end
end

It really depends on each how you want to identify and manipulate the array!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top