4

I am looking for a Javascript Algorithm to split an array into chunks, but avoiding any small left overs. For example:

_.chunk([1, 2, 3, 4, 5, 6, 7], 3) // [[1, 2, 3], [4, 5, 6], [7]]

But I want this:

_.chunk([1, 2, 3, 4, 5, 6, 7], 3) // [[1, 2, 3], [4, 5], [6, 7]]

_.chunk([1, 2, 3, 4, 5, 6, 7], 4) // [[1, 2, 3, 4], [5, 6, 7]]

_.chunk([1, 2, 3, 4, 5, 6, 7], 5) // [[1, 2, 3, 4], [5, 6, 7]]

_.chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3) // [[1, 2, 3], [4, 5, 6], [7, 8], [9, 10]]

So basically the output is spread over several arrays with a maximum number of elements passed in as second argument.

Rogier
  • 905
  • 1
  • 10
  • 25
  • 4
    what do you mean with *small leftovers*? – Nina Scholz Mar 21 '19 at 20:11
  • So what if in this example you would have passed 5 as second argument? Or 6? – trincot Mar 21 '19 at 20:17
  • can you please give some more example. What is expected output of `_.chunk([1, 2, 3, 4, 5, 6, 7, 8], 3)` – Maheer Ali Mar 21 '19 at 20:17
  • In the example above the lodash chunk function returns 1 element, while I prefer to avoid this element, but rather have 2 x 2 elements. – Rogier Mar 21 '19 at 20:18
  • If 5 or 6 are passed in as second argument the expected result should be `[[1, 2, 3, 4], [5, 6, 7]]` for both. – Rogier Mar 21 '19 at 20:20
  • For `_.chunk([1, 2, 3, 4, 5, 6, 7, 8], 3)` it would be `[[1, 2, 3], [4, 5, 6], [7, 8]]` – Rogier Mar 21 '19 at 20:21
  • So let me see if I can phrase what you are wanting. You want an algorithm that will chuck by an argument, but in the case that the last chuck is a single number, you want to regressively break up chucks until those total elements reach an even number so they can form chucks of two? – Taplar Mar 21 '19 at 20:22
  • 1
    *"If 5 or 6 are passed in as second argument the expected result should be [[1, 2, 3, 4], [5, 6, 7]] for both"*. This doesn't make any sense. Can you please update the question with some examples? – adiga Mar 21 '19 at 20:27
  • If 5 is passed in as second argument, then the splitted arrays are not longer then 5. The remained are 2 elements. As 5 and 2 are not 'balanced', you want to move one element to the other array. so `[[1, 2, 3, 4], [5, 6, 7]]`. The same applies for 6 as passed in as second element. – Rogier Mar 21 '19 at 20:31
  • Ok, so you want them to all be balanced, even if a certain number of them can match the chuck size, without a stragler being just 1? – Taplar Mar 21 '19 at 20:33
  • Correct! Although a strangler can be 1. For example `([1, 2, 3], 2)` -> `([1, 2], [3])`. – Rogier Mar 21 '19 at 20:35
  • @Rogier please update your question with more inputs and expected outputs. As it stands, it's unclear how the program should perform in a vast variety of input cases. – Mulan Mar 21 '19 at 20:45
  • So, chunks can differ by length one? `chunk(<11>, 5)` -> should return -> `4,4,3`? trincot's answer should solve this. – adiga Mar 21 '19 at 20:54
  • Try this, I think it can help you: https://stackoverflow.com/a/8495740 –  Mar 21 '19 at 20:56
  • Try this, I think it can help you: https://stackoverflow.com/a/8495740 –  Mar 21 '19 at 20:59
  • @Marom, please don't spam. – trincot Mar 21 '19 at 21:02
  • 1
    @trincot it's interesting though. They have 1 rep. So, they can't comment on posts. I'm assuming they are posting an answer and it's automatically being conrveted to a comment. Yep, found it: [How did someone with 1 rep post a comment?](https://meta.stackoverflow.com/questions/272265) – adiga Mar 21 '19 at 21:05
  • Good catch, @adiga! – trincot Mar 21 '19 at 21:14

2 Answers2

4

You should recalculate the size, which might need to be smaller than given. Then calculate from where the size should be one less for the remaining chunks.

So you will have potentially two different chunk sizes (which differ by 1). For both you can call the original _.chunk:

function chunk(arr, size) {
    const count = Math.ceil(arr.length / size);
    size = Math.ceil(arr.length / count);
    const i = arr.length-(size-1)*(arr.length%size && size-(arr.length%size));
    return _.chunk(arr.slice(0, i), size).concat(
           _.chunk(arr.slice(i), size-1));
}

for (let i = 1; i < 9; i++) {
    console.log(i, JSON.stringify(chunk([1, 2, 3, 4, 5, 6, 7], i)));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.4/lodash.min.js"></script>
trincot
  • 317,000
  • 35
  • 244
  • 286
0

This is done without lodash, it generates an array of proper length then fills in the parts with slices of the input array. It's only a few lines of code, depending how you count.

The caveat is the slices are up to how the decimals round. For your last example, you want to chunk a 10-length array with a limit of 3 per chunk. Well, dividing it out, you'll get the output:

[ [1,2], [3,4,5], [6,7], [8,9,10] ]

and not what you wanted:

[ [1,2,3], [4,5,6], [7,8], [9,10] ]

For most applications I don't think it matters much. I'm using this to chunk large inputs to an API that is throttled.

function chunk(array, limit) {
  const numChunks = Math.ceil(array.length / limit);
  return Array.from(
    { length: numChunks },
    (_, i) => array.slice(i * array.length / numChunks, (i + 1) * array.length / numChunks)
  );
}

console.log(chunk([1, 2, 3, 4, 5, 6, 7], 3));
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 4));
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 5));
console.log(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3));

An alternative solution uses recursion to chunk the "leftover" array. This one meets your criteria.

function chunk(array, limit) {
  if (array.length <= limit) return [array];
  const perChunk = Math.ceil(array.length / Math.ceil(array.length / limit));
  return [array.slice(0, perChunk)].concat(chunk(array.slice(perChunk), limit));
}

console.log(chunk([1, 2, 3, 4, 5, 6, 7], 3));
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 4));
console.log(chunk([1, 2, 3, 4, 5, 6, 7], 5));
console.log(chunk([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3));
000
  • 26,951
  • 10
  • 71
  • 101