loading...

Array Chunking

ryanfarney3 profile image Ryan Farney ・3 min read

By array chunking, I mean taking an entire array and creating one array containing smaller subarrays of the original array's elements. Not only is this a commonly asked concept on technical interviews, but I can also see how there may be use cases for it when ordering or organizing a dataset.

Here I will tackle a relatively simple "array chunking" problem and go through a few different ways of solving it. By no means are these the only ways of doing it!

Problem

Given an array and chunk size, divide the array into many subarrays where each subarray is of length size.

I believe conceptualizing this problem is easier when we can see some of the expected outputs...

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

Here we can see that the "size" number is for how many elements are in each subarray. As we can see they are not always even, so we want to make sure that the extra elements in our original array will plug into a final and smaller subarray.

Alrighty! I will throw in line by line explanations, but... Let’s code already ☺.

First Solution

This is the solution that is likely a bit more evident, especially to somebody who is not as practiced in JavaScript. Orrrr maybe your interviewer asks you to solve the problem without a few of the fancier JS methods. You never know!

function chunk(array, size) {
  //declaring variable 'chunked' as an empty array
  let chunked = [];

  //for loop iterating through every element of our input array
  for (let ele of array) {
    //declaring variable 'last' as the last index of our 'chunked' array
    const last = chunked[chunked.length-1];

    //checking if last is undefined or if the last subarray is equal to the size
    if (!last || last.length === size) {
      //then we push the element to be a new subarray in 'chunked'
      chunked.push([ele])
    } else {
      //if not, then we add the element to the 'last' subarray
      last.push(ele)
    }
  }
  //return the array of subarrays
  return chunked
}

Second Solution

In this second solution (and probably the one that comes the most naturally to me) we make use of the .slice method. If you are unfamiliar, please reference the docs for .slice here! The key thing to remember here is that calling .slice will return a new array!

function chunk(array, size) {
  //declaring variable 'chunked' as an empty array
  let chunked = []

  //setting our start point for our while loop at index 0
  let i = 0;
  //looping through the array until we have reached the final index
  while (i < array.length) {
    //push the sliced subarray of length 'size' into our new 'chunked' array
    chunked.push(array.slice(i, i + size))
    //increment by the size as to avoid duplicates
    i += size;
  }
  //return the array of subarrays
  return chunked
}

Third Solution

This last solution is probably the fanciest (and shortest). Personally, I have not gotten super familiar with the .splice method, but I know it can occasionally lend itself handy in problems like these. Again, please reference the docs here! Specifically for this solution's needs, I would scroll down a bit to reference how it is used when removing elements... but it can also do a few other things. The key thing to remember here is that, unlike .slice, the .splice method mutates the original array!

function chunk(array, size) {
  //declaring variable 'chunked' as an empty array
  let chunked = []

  //looping through the array until it has been entirely "manipulated" or split into our subarrays
  while(array.length > 0) {
    //taking the spliced segments completely out of our original array
    //pushing these subarrays into our new "chunked" array
    chunked.push(array.splice(0, size))
  }
  //returning the new array of subarrays
  return chunked
}

Thanks For Stopping By!

Posted on by:

ryanfarney3 profile

Ryan Farney

@ryanfarney3

Full stack web developer with a passion for consistency, learning from my mistakes and using my gifts to make a significant difference. Experience in Ruby on Rails, Javascript, React

Discussion

markdown guide
 

Nothing beats a recursive solution when it comes to elegance IMO.

const chunk = (xs, n) => {
  if (xs.length <= n) return [xs];
  return [
    xs.slice(0, n),
    ...chunk(xs.slice(n), n)
  ];
};
 

If you're already in the mood for elegance, why not use a single expression arrow function?

const chunk = (xs, n) => (xs.length <= n) ? [xs] : [
  xs.slice(0, n),
  ...chunk(xs.slice(n), n)
];
 

because IMO that's not more elegant.

I think guard clauses should live in the if statement that has nothing to do with the rest of the code. I think it is easier to read.

But I believe there is no absolute "elegance". Programming is just like art, different people perceive a piece differently.

Mm, Jason's version looks more readable to me. It's laid out a little more like you would if you were matching patterns in Haskell or something.

 

Here are my 2 cents. It is performant and avoids recursion.

const chunk = (xs, n) => xs.reduce((o, x, i) => {
  o[Math.floor(i/n)].push(x);
  return o;
}, Array(Math.ceil(xs.length/n)).fill([]))
 

It's funny where a practitioner's blindspots can grow. I literally wouldn't have known exactly what array chunking was. I could guess, but the specific named concept just was not part of my vocabulary.

Just goes to show how random and non-linear the software learning curve is.

 

This is generally how I do it if I need to partition and I don't have a utility library.

const splitEvery = (arr, n) => arr.reduce((memo, val) => {
  if(memo[memo.length - 1].length === n) memo.push([]);
  memo[memo.length - 1].push(val);
  return memo;
}, [[]]);
 
 

Can you elaborate why is that?

 

This is because Array.prototype.splice does whatever it does in place. Meaning that it mutates the original array. Mutation are usually consider to make code hard to reason about unless it is very explicit (like obj.setName('jason')).

Going back to the chunk example, consider the following code:

const arr = [1,2,3,4,5,6,7,8,9];
const chunks = chunk(arr, 3);

console.log(chunks);
console.log(arr);

What would you expect to be logged? I guess you would probably expect:

[[1,2,3], [4,5,6], [7,8,9]] // chunks
[1,2,3,4,5,6,7,8,9] // arr

But since Ryan suggested to use splice, it edits our arr in place. And the actual log you will see is:

[[1,2,3], [4,5,6], [7,8,9]] // chunks
[] // arr

A way to fix this is to clone the whole array before doing anything.

function chunk(array, size) {
  // clone the array to avoid touching the original one.
  array = array.slice(0)
  //declaring variable 'chunked' as an empty array
  let chunked = []

  //looping through the array until it has been entirely "manipulated" or split into our subarrays
  while(array.length > 0) {
    //taking the spliced segments completely out of our original array
    //pushing these subarrays into our new "chunked" array
    chunked.push(array.splice(0, size))
  }
  //returning the new array of subarrays
  return chunked
}

Or a more ES6+ method using array spread (preferable):

function chunk([...array], size) {
  //declaring variable 'chunked' as an empty array
  let chunked = []

  //looping through the array until it has been entirely "manipulated" or split into our subarrays
  while(array.length > 0) {
    //taking the spliced segments completely out of our original array
    //pushing these subarrays into our new "chunked" array
    chunked.push(array.splice(0, size))
  }
  //returning the new array of subarrays
  return chunked
}

I hope I explained this properly.

Thank you Basit and Jason for clearing this up! I certainly should have mentioned the dangers of mutating our original dataset.

Nonetheless, having knowledge of how to use .splice as well as it's dangers could show mastery of JS in the context of an interview <3

 

Or something like:

const chunk = (arr, n) => [...new Array(Math.ceil(arr.length / n))].map((x, i) => arr.slice(i * n, (i + 1) * n ))
 

That's kind of hard to read (mostly because it's all on one loooong line).

 

Or even shorter 🤣

const chunk = ([...arr], n) => [...Array(Math.ceil(arr.length) / n)].map(_ => arr.splice(0, n))

 

another simple loop version

  const toChunks = (arr, chunkLength) => {
    const chunks = []
    let i, j, chunk
    for (i = 0, j = arr.length; i < j; i += chunkLength) {
      chunk = arr.slice(i, i + chunkLength)
      chunks.push(chunk)
    }
    return chunks
  }
 

Learned something new today. ! ~_~

But I am unable to see the use case for this. Why would I need to do chunking in the first place?

 

When you need to fetch something from a 3rd party server and that server only accepts 1000 ids.. So you may need chunking for your 5000 id numbers :)