1

I have an issue here, I'm trying to create a function that sums up all integers from a deeply nested array, but it's failing an unit test, which means something is not right. Here is my function:

export const arraySum = (arr) => {
  let sum = 0;
  for (let i = 0; i < arr.length; i++) {
    if (typeof arr[i] === "number") sum = sum + arr[i];
    else if (Array.isArray(arr[i])) sum = sum + arraySum(arr[i]);
  }
  return sum;
};

And here is my unit test which is failing:

test("it should sum up from deeply nested arrays", () => {
    type ValueOrArray = number | Array<ValueOrArray>;
    const createDeeplyNestedArray = (depth: number): ValueOrArray => {
      let retval: ValueOrArray = [1];
      for (let i = 0; i < depth - 1; i++) {
        retval = [1, retval];
      }
      return retval;
    };

    const NUMBER_OF_ELEMENTS = 100000;
    const arr = createDeeplyNestedArray(NUMBER_OF_ELEMENTS);
    expect(arraySum(arr)).toEqual(NUMBER_OF_ELEMENTS);
  });
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
Christian
  • 53
  • 1
  • 8
  • Because a call stack of 100000 is massive.. :) You will need to alter your function to NOT use recursion here for an array that deep. – Keith Jun 16 '22 at 20:38
  • @Keith do you have any ideas how to alter the function? I'm not able to find other solutions for this issue – Christian Jun 16 '22 at 20:40
  • Are you sure you need an array that is 100,000 levels deep?. It is possible to alter this to work. Tail Call Optimzation is something that would work here, unfortunatly it seems most JS engines don't implement it, but there is a strange workaround at this link -> https://stackoverflow.com/questions/37224520/are-functions-in-javascript-tail-call-optimized – Keith Jun 16 '22 at 20:46
  • One possibility is to use setTimeout with duration 0, but then you probably have to use a callback function as parameter to process the result. – maraca Jun 16 '22 at 20:46
  • @Keith I see that it only works on Firefox or only on Safari from what people are saying there, but I have a requirement that the solution needs to work in all common browsers so I don't think this would be ok, unfortunately. – Christian Jun 16 '22 at 20:51
  • @maraca can you give me an example? I'm not sure I understand how to use a timeout function here – Christian Jun 16 '22 at 20:52
  • @Christian10 No it will work, look at the workarround, the workarround wasn't just for Safari. IOW: The post by `tjjfvi` eg. His Factorial works for me in Chrome. – Keith Jun 16 '22 at 20:56
  • @Christian10 it's not worth it, Ben Wainwright's solution is much better. It would get very ugly. – maraca Jun 16 '22 at 21:09
  • @PeterSeliger I don't think so, the solution should be in the implementation, not on the test side. At least, this is what is given to me, to write the solution in function arraySum. – Christian Jun 16 '22 at 21:29

5 Answers5

2

That doesn't work bc like Keith said you are reaching the maximum call stack size.

RangeError: Maximum call stack size exceeded is a type of error thrown in JavaScript when a function call is made that exceeds the program's stack size. The call stack is a data structure that keeps track of which functions have been called, and in what order.

Maybe you can try to solve it in a iterative way like this:

 const arraySum = (arr) => {
  if (!Array.isArray(arr)) return 0;
  let sum = 0;
  while (Array.isArray(arr)) {
    let arrCopy = [];
    for (let i = 0; i < arr.length; i++) {
      if (typeof arr[i] === "number") sum = sum + arr[i];
      else if (Array.isArray(arr[i])) arrCopy = arrCopy.concat(arr[i]);
    }
    arr = arrCopy.length > 0 ? arrCopy : null;
  }
  return sum;
};
Gustavo A Olmedo
  • 565
  • 4
  • 13
  • Thanks for your help, but unfortunately I get the same "Maximum call stack size exceeded" error. – Christian Jun 16 '22 at 21:01
  • 1
    I'm thinking that maybe the createDeeplyNestedArray function is the one that is crashing. – Gustavo A Olmedo Jun 16 '22 at 21:11
  • @GustavoAOlmedo No, it's fine.. – Keith Jun 16 '22 at 21:25
  • @GustavoAOlmedo I don't think so, it is requested to write the solution in function arraySum. I don't think that I should change something in the unit test. – Christian Jun 16 '22 at 21:31
  • I think if you use the function which is my answer with 100000 for the NUMBER_OF_ELEMENTS variable, you shouldn't have any issue. If you instead increase that number to something equal or greater than 100000000, the createDeeplyNestedArray should crash. Or at least that's the behaviour I have in m y machine :'( – Gustavo A Olmedo Jun 16 '22 at 21:39
  • @GustavoAOlmedo I've tried your solution in implementation, not changed anything in the unit test side, but I get the same error... I can't explain why. Do you propose to change somehow the unit test? But I don't think that the solution requires this, I was told that the solution should be on the implementation side. – Christian Jun 16 '22 at 21:45
  • If you are testing it with 100000 for NUMBER_OF_ELEMENTS, and you still get an error, the bug should be in another place, can you add a screenshot or something like that? – Gustavo A Olmedo Jun 16 '22 at 21:46
  • 1
    actually is working. I needed to refresh the browser because it was not working with the refresh from codesandbox.. so there it was the bug lol :) Thanks @GustavoAOlmedo for your help. – Christian Jun 16 '22 at 22:25
2

Function memory is stored on something called a "call stack". Whenever you call a function, all of its variables are allocated and pushed onto the 'stack' and when the function returns, they are popped off the stack. Given the following code:

const a = () => {
}

const b = () => {
   a()
   // some code
}

const c = () => {
   b()
}

c()

When c is called, your call stack will contain all the memory for variables used in c. When c calls b, all the memory for variables used in b are added to the stack. When b calls a all the memory for variables used in a are added to the stack. When a finishes executing (so when you get to 'some code'), variables related to a are deallocated and removed from the stack.

The problem you have here is that every time your function recursively calls itself, more memory is being allocated onto the stack. to stop this kind of code using up all the system memory, the runtime limits how big the stack can get - which is why you are hitting this error.

To pass this test, you need a solution which doesn't call itself every time it hits an array within an array. Here's my solution, effectively using an array as a buffer; each time I hit a nested array I add it to the buffer. Once I finish processing the outer array, I then check if there is any arrays left in the buffer.

export const arraySum = (arr) => {
  let sum = 0;
  const buffer = [arr];
  while (buffer.length > 0) {
    const next = buffer.shift();
    for (let i = 0; i < next.length; i++) {
      if (typeof next[i] === "number") sum = sum + next[i];
      else if (Array.isArray(next[i])) buffer.push(next[i]);
    }
  }
  return sum;
};
Ben Wainwright
  • 4,224
  • 1
  • 18
  • 36
  • Thanks for helping me. I tried this solution now, but I'm getting the same error "Maximum call stack size exceeded", it works on your side? – Christian Jun 16 '22 at 21:17
  • @Christian10 You shouldn't be, this one is working fine. And you certainly shound'nt be getting a stack error here as there is no recursion. – Keith Jun 16 '22 at 21:39
  • 1
    actually this is working yes, you are right Keith. I needed to refresh codesandbox in order to work .. lol. It wasn't working with their "refresh". Thanks @Ben ! – Christian Jun 16 '22 at 22:13
2

We can use a stack or queue in place of recursion. Also, order doesn't matter.

function f(A) {
  let sum = 0;
  let stack = [A];
  
  while (stack.length > 0) {
    stack.pop().forEach(e => {
      if (Array.isArray(e))
        stack.push(e);
      else if (typeof e === "number")
        sum += e;
    });
  }
  
  return sum;
}

console.log(f([1,2,[3,[4]]]));
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • 1
    @Christian10 ... straightforward (and performant) and thus, elegant. – Peter Seliger Jun 16 '22 at 22:47
  • 1
    Agreed, very performant! However, you shouldn't declare functions inside a loop! Moving that out would make this a great answer IMO – vincent Jun 19 '22 at 19:14
  • @vincent sorry, I don't follow. What function is declared inside a loop? – גלעד ברקן Jun 19 '22 at 20:58
  • The function `e => { if (Array.isArray(e)) stack.push(e); else if (typeof e === "number") sum += e; }` is declared inside the while loop – vincent Jun 20 '22 at 14:26
  • @vincent ah, interesting. There's a performance difference if the inner method is declared elsewhere? Can you please show that performance difference somehow? – גלעד ברקן Jun 20 '22 at 14:39
  • I don't think this is a performance problem (it might be?), but it's definitely bad practice. There is an eslint rule that explains it well! https://eslint.org/docs/rules/no-loop-func – vincent Jun 20 '22 at 14:43
1

The approach of combining a flat based flattening function which circumvents call stack errors of very deeply nested arrays of a nesting depth close to 10_000 and higher and a reduce based function which does sum-up number types only, does solve the OP's problem. And the following implementation does prove it ...

// Implementation
function flatAlmostInfiniteNestedArray(arr) {
  while (arr.length < (arr = arr.flat(1000)).length) {
  }
  return arr;
}
function totalNestedNumberValues(arr) {
  return flatAlmostInfiniteNestedArray(arr)
    .reduce((total, value) => (
      'number' === typeof value
        ? total + value
        : total
    ), 0);
}

// Test
const createDeeplyNestedArray = depth => {
  let retval = [1];
  for (let i = 0; i < depth - 1; i++) {
    retval = [1, retval];
  }
  return retval;
};
const NUMBER_OF_ELEMENTS = 100000;
const arr = createDeeplyNestedArray(NUMBER_OF_ELEMENTS);

console.log(
  '(totalNestedNumberValues(arr) === NUMBER_OF_ELEMENTS) ?..',
  (totalNestedNumberValues(arr) === NUMBER_OF_ELEMENTS),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit ... due to following comments

"Very interesting. I hadn't realized that Array.flat was implemented in a way that would lead to a call stack overflow. It makes sense that it would be, but I didn't know, and have never had a real-world structure where it would matter. Thanks!" – Scott Sauyet

"@ScottSauyet ...the poor man's approach of (mis)using flat as kind of a callstack-safe solution looses if it comes to performance ... jsbench.me :: sum up deeply nested array's number values" – Peter Seliger

One of the above linked performance tests features a better, much more performant, stack based, solution for flattening deeply nested arrays which are critical of being natively flattened due to possible overflowing call stacks.

Thus the formerly provided code example would change to ...

// Implementation
function flatCallstackCriticalNestedArray(nested) {
  const stack = [nested];
  const flat = [];
  let value;

  while (stack.length) {
    if (Array.isArray(value = stack.pop())) {
    
      stack.push(...value);
    } else {
      flat.push(value);
    }
  }
  return flat;
}
function totalNestedNumberValues(arr) {
  return flatCallstackCriticalNestedArray(arr)
    .reduce((total, value) => (
      'number' === typeof value
        ? total + value
        : total
    ), 0);
}

// Test
const createDeeplyNestedArray = depth => {
  let retval = [1];
  for (let i = 0; i < depth - 1; i++) {
    retval = [1, retval];
  }
  return retval;
};
const NUMBER_OF_ELEMENTS = 100000;
const arr = createDeeplyNestedArray(NUMBER_OF_ELEMENTS);

console.log(
  '(totalNestedNumberValues(arr) === NUMBER_OF_ELEMENTS) ?..',
  (totalNestedNumberValues(arr) === NUMBER_OF_ELEMENTS),
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
  • 1
    Very interesting. I hadn't realized that `Array.flat` was implemented in a way that would lead to a call stack overflow. It makes sense that it would be, but I didn't know, and have never had a real-world structure where it would matter. Thanks! – Scott Sauyet Jun 17 '22 at 01:49
  • 1
    @ScottSauyet ...the poor man's approach of (mis)using `flat` as kind of a callstack-safe solution looses if it comes to performance ... [jsbench.me :: sum up deeply nested array's number values](https://jsbench.me/vtl4i48ucu/1) – Peter Seliger Jun 17 '22 at 07:44
0

Have you considered a using library? This might not be as fast as a vanilla solution, but it should still be plenty fast and much more readable.

.as-console-wrapper {max-height: 100% !important; top: 0}
<script type="module">
import objectScan from 'https://cdn.jsdelivr.net/npm/object-scan@18.3.0/lib/index.min.js';

const createDeeplyNestedArray = (depth) => {
  let retval = [1];
  for (let i = 0; i < depth - 1; i += 1) {
    retval = [1, retval];
  }
  return retval;
};

const NUMBER_OF_ELEMENTS = 100000;
const arr = createDeeplyNestedArray(NUMBER_OF_ELEMENTS);

const arraySum = objectScan(['**'], {
  rtn: 'sum',
  filterFn: ({ value }) => typeof value === 'number'
});
console.log(arraySum(arr));
// => 100000
</script>

Disclaimer: I'm the author of object-scan

Note that this will traverse arrays and objects. If you only want to traverse arrays you could use ['**{[*]}'] as search needles.

vincent
  • 1,953
  • 3
  • 18
  • 24