Dividing elements of a ruby array into an exact number of (nearly) equal-sized sub-arrays [duplicate]
Asked Answered
U

5

73

I need a way to split an array in to an exact number of smaller arrays of roughly-equal size. Anyone have any method of doing this?

For instance

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] 
groups = a.method_i_need(3)
groups.inspect
    => [[1,2,3,4,5], [6,7,8,9], [10,11,12,13]]

Note that this is an entirely separate problem from dividing an array into chunks, because a.each_slice(3).to_a would produce 5 groups (not 3, like we desire) and the final group may be a completely different size than the others:

[[1,2,3], [4,5,6], [7,8,9], [10,11,12], [13]]  # this is NOT desired here.

In this problem, the desired number of chunks is specified in advance, and the sizes of each chunk will differ by 1 at most.

Unreconstructed answered 11/9, 2012 at 17:9 Comment(3)
It's unfortunate that the chosen example has the same result for "split in 3 groups" and "split in groups of 3 elements", that's why you got two completely different answers.Faerie
This question is different to the linked question. The linked question is to split an array into equal, known, sizes; this question is to split an array into an equal number of chunks, each of similar size.Currant
Seconded: this is not a duplicate of the linked question, and this is the top result for a seach for "ruby slice array into n equal parts". Here is my Ruby-only (Rails not required) answer to the same problem on another question: stackoverflow.com/a/63040779Brownedoff
B
135

You're looking for Enumerable#each_slice

a = [0, 1, 2, 3, 4, 5, 6, 7]
a.each_slice(3) # => #<Enumerator: [0, 1, 2, 3, 4, 5, 6, 7]:each_slice(3)>
a.each_slice(3).to_a # => [[0, 1, 2], [3, 4, 5], [6, 7]]
Brianna answered 11/9, 2012 at 17:12 Comment(3)
Just a note. This splits the array into groups of size 3. not into 3 equal sized groups.Tannate
If the array size does not divide evenly into the number of slices, is it possible to merge the remainder slice with the previous slice? Given your example, [6, 7] would be merged with [3, 4, 5] to make [3, 4, 5, 6, 7].Aspa
@BorisStitnicky That probably speaks to the bad UX of documentation sites. Sometimes, it's easier to search by what you want to do, rather than having to know which class has the method you want. Googling is the thing, and if the results are SO, then that is it. There are noobs as well as programmers unfamiliar with a given language that use this. Skill is a pyramid, and there will be more noobs than experts.Melodie
S
126

Perhaps I'm misreading the question since the other answer is already accepted, but it sounded like you wanted to split the array in to 3 equal groups, regardless of the size of each group, rather than split it into N groups of 3 as the previous answers do. If that's what you're looking for, Rails (ActiveSupport) also has a method called in_groups:

a = [0,1,2,3,4,5,6]
a.in_groups(2) # => [[0,1,2,3],[4,5,6,nil]]
a.in_groups(3, false) # => [[0,1,2],[3,4], [5,6]]

I don't think there is a ruby equivalent, however, you can get roughly the same results by adding this simple method:

class Array; def in_groups(num_groups)
  return [] if num_groups == 0
  slice_size = (self.size/Float(num_groups)).ceil
  groups = self.each_slice(slice_size).to_a
end; end

a.in_groups(3) # => [[0,1,2], [3,4,5], [6]]

The only difference (as you can see) is that this won't spread the "empty space" across all the groups; every group but the last is equal in size, and the last group always holds the remainder plus all the "empty space".

Update: As @rimsky astutely pointed out, the above method will not always result in the correct number of groups (sometimes it will create multiple "empty groups" at the end, and leave them out). Here's an updated version, pared down from ActiveSupport's definition which spreads the extras out to fill the requested number of groups.

def in_groups(number)
  group_size = size / number
  leftovers = size % number

  groups = []
  start = 0
  number.times do |index|
    length = group_size + (leftovers > 0 && leftovers > index ? 1 : 0)
    groups << slice(start, length)
    start += length
  end

  groups
end
Silvanus answered 29/11, 2012 at 20:47 Comment(5)
I know this is an old post, but for those who are considering the ruby equivalent above, it's not quite correct. If you try to split an array of 20 elements into 11 groups, you'll end up with only 10 groups. slice_size will be 2, and 20 is divisible by 2.Trawick
This is what I came here looking for. Not for groups of n size, like the accepted answer. Thank you.Eous
Nice catch @rimsky! Updated ;)Silvanus
It appears @rimsky's comment is not longer correct, at least of ActiveSupport 4.1.16. running on an array with 20 elements works: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9].in_groups(11).count => 11Angelenaangeleno
The ActiveSupport version has always worked. @Trawick was pointing out that my original (simplified) method above had that flaw.Silvanus
M
16

Try

a.in_groups_of(3,false)

It will do your job

Malinda answered 11/9, 2012 at 17:12 Comment(3)
Note that in_groups_of is specific to Rails (or rather, ActiveSupport), while @Joshua's answer is usable in Ruby everywhere. Still, +1 for providing a working solution.Thyrotoxicosis
It's also only on Array.Brianna
Note that the second parameter is the filler value (by default nil) in case array size is not dividable by the first parameter.Sangfroid
M
5

As mltsy wrote, in_groups(n, false) should do the job.

I just wanted to add a small trick to get the right balance my_array.in_group(my_array.size.quo(max_size).ceil, false).

Here is an example to illustrate that trick:

a = (0..8).to_a
a.in_groups(4, false) => [[0, 1, 2], [3, 4], [5, 6], [7, 8]]
a.in_groups(a.size.quo(4).ceil, false) => [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
Mallen answered 10/3, 2017 at 14:47 Comment(0)
S
3

This needs some better cleverness to smear out the extra pieces, but it's a reasonable start.

def i_need(bits, r)
  c = r.count
  (1..bits - 1).map { |i| r.shift((c + i) * 1.0 / bits ) } + [r]
end

>   i_need(2, [1, 3, 5, 7, 2, 4, 6, 8])
 => [[1, 3, 5, 7], [2, 4, 6, 8]] 
> i_need(3, [1, 3, 5, 7, 2, 4, 6, 8])
 => [[1, 3, 5], [7, 2, 4], [6, 8]] 
> i_need(5, [1, 3, 5, 7, 2, 4, 6, 8])
 => [[1, 3], [5, 7], [2, 4], [6], [8]] 
Spic answered 6/11, 2017 at 9:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.