3

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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
mila002
  • 327
  • 3
  • 14
  • "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 Aug 10 '20 at 18:59

2 Answers2

2

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
Kumar
  • 3,116
  • 2
  • 16
  • 24
  • You're using `reduce` as an `each`, to cause side-effects. Don't do that. :) We can easily rewrite it so that reduce does not cause side-effects and returns the final array directly. – Sergio Tulentsev Aug 10 '20 at 19:17
  • Thanks a lot, Sergio! I've updated the code reducing it an array instead. :) Would love to more know more about the side effect. Any references will be appreciated! – Kumar Aug 10 '20 at 19:44
  • @SergioTulentsev missed to mention you. :P – Kumar Aug 10 '20 at 19:52
  • ah yes, that's exactly what I meant :) And by "side effect" I meant that your reduce block is changing external state (that array) instead of using only the accumulator and the current element. – Sergio Tulentsev Aug 10 '20 at 19:55
  • Got it. :) Thanks a lot for the input. Cheers! – Kumar Aug 10 '20 at 20:00
  • @Kumar could You explain me that code? I'm little consfused of using reduce. – mila002 Aug 10 '20 at 22:26
  • @mila002 added explanation – Kumar Aug 11 '20 at 07:17
2
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.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100