3
votes

I have an array of subarrays:

arr = [["a", "b", "c"], ["a", "b"], ["a", "b", "c"], ["a", "c"],
       ["c", "v"], ["c", "f"], ["e", "a"], ["a", "b", "v"],
       ["a", "n", "c"], ["a", "b", "m"], ["a", "c"], ["a", "c", "g"]]

I want to put elements of every subarray into another array but the sum of subarrays size must be less or equal to 6. So I want to get something like this

[["a", "b", "c", "a", "b"], ["a", "b", "c", "a", "c"],
 ["c", "v", "c", "f", "e", "a"], ["a", "b", "v", "a", "n", "c"],
 ["a", "b", "m", "a", "c"], ["a", "c", "g"]]

My code for now is

stop = 0
new_arr = []
indexo = ""
arr.each_with_index do |x, index|
   stop = stop + x.size
   if stop <= 6
      new_arr << x
      indexo = index
   end
end

And I am stuck here because my code takes only two first elements. Original array has about 1000 subarrays and my code does not split it in that form.

2
"because my code takes only two first elements" - no, it takes them all, but it does not do much for the third and beyond, because stop variable only grows up (and the third element makes it exceed 6).Sergio Tulentsev

2 Answers

2
votes

You can use reduce method and keep pushing sub arrays to a new array. Consider the following:

new_arr = arr.reduce([]) do |acc, sub_array|
  last_element = acc[acc.length - 1]

  if last_element.nil? or (last_element + sub_array).length > 6
    acc << sub_array
  else
    acc[acc.length - 1] = last_element + sub_array
  end
  acc
end

# Tests
new_arr.flatten.size == arr.flatten.size # test total number of elements in both the arrays
new_arr.map(&:size) # the sizes of all sub arrays
new_arr.map(&:size).min # min size of all sub arrays
new_arr.map(&:size).max # max size of all sub arrays

Let me know if the code is not clear to you

Update:

Reduce method will "reduce" any enumerable object to a single value by iterating through every element of the enumerable just like each, map

Consider an example:

# Find the sum of array
arr = [1, 2, 3]

# Reduce will accept an initial value & a block with two arguments
#   initial_value: is used to set the value of the accumulator in the first loop

#   Block Arguments:
#   accumulator: accumulates data through the loop and finally returned by :reduce
#   value: each item of the above array in every loop(just like :each)

arr.reduce(0) do |acc, value|
  # initial value is 0; in the first loop acc's value will be set to 0
  # henceforth acc's value will be what is returned from the block in every loop

  acc += value
  acc # acc is begin returned; in the second loop the value of acc will be (0 + 1)
end

So in this case in every loop, we add the value of the item to the accumulator and return the accumulator for use in the next loop. And once reduce has iterated all the items in the array it will return the accumulator.

Ruby also provides syntactic sugar to make it look much fancier:

arr.reduce(:+) # return 6

Here's a good article for further reference

So if you take your question for example:

# Initial value is set to an empty array, what we're passing to reduce
new_arr = arr.reduce([]) do |acc, sub_array|
  # In the first loop acc's value will be set to []

  # we're finding the last element of acc (in first loop since the array is empty
  #    last element will be nil)
  last_element = acc[acc.length - 1]

  # If last_element is nil(in first loop) we push the first item of the array to acc
  # If last_element is found(pushed in the previous loops), we take it and sum
  #    it with the item from the current loop and see the size, if size is more
  #    than 6, we only push the item from current loop
  if last_element.nil? or (last_element + sub_array).length > 6
    acc << sub_array
  else
    # If last element is present & last_element + item from current loop's size
    #    is less than 6, we push the (last_element + item from current loop) into 
    #    the accumulator.
    acc[acc.length - 1] = last_element + sub_array
  end

  # Finally we return the accumulator, which will be used in the next loop
  # Or if has looped through the entire array, it will be used to return back
  #    from where it was called
  acc
end
2
votes
arr = [["a", "b", "c"], ["a", "b"], ["a", "b", "c"], ["a", "c"],
       ["c", "v"], ["c", "f"], ["e", "a"], ["a", "b", "v"],
       ["a", "n", "c"], ["a", "b", "m"], ["a", "c"], ["a", "c", "g"]]
arr.each_with_object([[]]) do |a,ar|
  if a.size + ar[-1].size > 6
    ar << a
  else
    ar[-1] += a
  end
end
  #=> [["a", "b", "c", "a", "b"], ["a", "b", "c", "a", "c"],
  #    ["c", "v", "c", "f", "e", "a"], ["a", "b", "v", "a", "n", "c"],
  #    ["a", "b", "m", "a", "c"], ["a", "c", "g"]]

The steps are as follows.

enum = arr.each_with_object([[]])
  #=> #<Enumerator: [["a", "b", "c", "a", "b"], ["a", "b"],...
  #     ["a", "c", "g"]]:each_with_object([[]])>

The first value is generated by this enumerator, passed to the block and the block values are assigned values by applying Array Decomposition to the two-element array passed to the block.

a, ar = enum.next
   #=> [["a", "b", "c"], [[]]] 
a  #=> ["a", "b", "c"] 
ar #=> [[]] 

See Enumerator#next. The conditional statement is then evaluated.

a.size + ar[-1].size > 6
  #=> 3 + 0 > 6 => false

so we execute:

ar[-1] += a
   #=> ["a", "b", "c"] 
ar #=> [["a", "b", "c"]]

The next element is generated by enum, passed to the block and the block values are assigned values.

a, ar = enum.next
   #=> [["a", "b"], [["a", "b", "c"]]] 
a  #=> ["a", "b"] 
ar #=> [["a", "b", "c"]]

The conditional statement is evaluated.

a.size + ar[-1].size > 6
  #=> 2 + 3 > 6 => false

so again we execute:

ar[-1] += a
   #=> ["a", "b", "c", "a", "b"] 
ar #=> [["a", "b", "c", "a", "b"]]

enum then passes the third element to the block.

a, ar = enum.next
   #=> [["a", "b", "c"], [["a", "b", "c", "a", "b"]]] 
a  #=> ["a", "b", "c"] 
ar #=> [["a", "b", "c", "a", "b"]] 

Because:

a.size + ar[-1].size > 6
  #=> 3 + 5 > 6 => false

this time we exectute

ar << a
  #=> [["a", "b", "c", "a", "b"], ["a", "b", "c"]] 

The remaining steps are similar.