Search code examples
javascriptarrayspushreducelogical-and

From simple array to 2D array using reduce (ft. logical AND &&)


I needed to "convert" a simple flat array into a 2D array and I went on SO to see what it has to say about the argument.
I tried to recreate the code of this answer, and I got this error:

console.log(array.reduce((twoDArray, n, i) => (i % 3 == 0 ? twoDArray.push([n]) : twoDArray[twoDArray.length-1].push(n)), []));
                                                                                                                ^
TypeError: Cannot read property 'push' of undefined

The problem was that I didn't add && twoDArray at the end of the arrow function. Here you can see:

let array = [1,2,3,4,5,6,7,8,9];

// this works
console.log(array.reduce((twoDArray, n, i) => (i % 3 == 0 ? twoDArray.push([n]) : twoDArray[twoDArray.length-1].push(n)) && twoDArray, []));

// here the second push() throws an error
console.log(array.reduce((twoDArray, n, i) => (i % 3 == 0 ? twoDArray.push([n]) : twoDArray[twoDArray.length-1].push(n)), []));

Now I don't understand a couple of things, namely:

  • how does this && twoDArray works? what's its purpose?
  • how can this addition fix the error when it is placed only after the push() that generates the error. Shouldn't the code throw an error before to reach the &&?

Solution

  • This is needed because push returns the new length of the array - but the accumulator needs to be the array, not the length.

    Without the &&, and indenting the code into multiple lines to make it clearer what's going on, the second code is equivalent to:

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    // here the second push() throws an error
    console.log(array.reduce((twoDArray, n, i) => {
      return (i % 3 == 0 ? twoDArray.push([n]) : twoDArray[twoDArray.length - 1].push(n))
    }, []));

    Same as:

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    // here the second push() throws an error
    console.log(array.reduce((twoDArray, n, i) => {
      return (
        i % 3 == 0
          ? twoDArray.push([n])
          : twoDArray[twoDArray.length - 1].push(n)
      );
    }, []));

    Now, the problem should be clear: no matter which condition is entered, the callback evaluates to

      return (
        i % 3 == 0
          ? someNumber
          : someNumber
      );
    

    because .push evaluates to the new length of the array.

    Adding && twoDArray to it makes the callback look like:

      return (
        i % 3 == 0
          ? someNumber
          : someNumber
      ) && twoDArray;
    

    therefore returning twoDArray instead of the number.

    Shouldn't the code throw an error before to reach the &&?

    It does. The error is thrown on the second iteration, when twoDArray[twoDArray.length-1], when twoDArray is a number, evaluates to undefined, so it can't be pushed to. But the problem that twoDArray is a number instead of an array results from the code at the tail end of the prior (first) iteration: the lack of the && twoDArray;.

    Code like this is extremely confusing. Try not to condense code into a single line if it makes it unreadable. Another issue is that .reduce arguably isn't appropriate when the accumulator is the same object on each iteration. Consider instead doing something like this:

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    const twoDArray= [];
    array.forEach((n, i) => {
      i % 3 == 0
        ? twoDArray.push([n])
        : twoDArray[twoDArray.length - 1].push(n);
    });
    console.log(twoDArray);

    And use if/else instead of the conditional operator:

    let array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    
    const twoDArray= [];
    array.forEach((n, i) => {
      if (i % 3 === 0) twoDArray.push([n])
      else twoDArray[twoDArray.length - 1].push(n);
    });
    console.log(twoDArray);