Keep track of how many times a recursive function was called

This is an almost purely academic variant, but you can use a modified fixed point combinator for this purpose.

Lets shorten and improve your original function a bit:

function singleDigit(n) {
    let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
    return digitProduct <= 9 ? digitProduct : singleDigit(digitProduct);
}

// singleDigit(123234234) == 0

From this variant, we can factor out and curry the recursive call:

function singleDigitF(recur) {
    return function (n) {
        let digitProduct = [...(n + '')].reduce((x, y) => x * y, 1);
        return digitProduct <= 9 ? digitProduct : recur()(digitProduct);
    };
}

This function can now be used with a fixed point combinator; specifically I implemented a Y combinator adapted for (strict) JavaScript as follows:

function Ynormal(f, ...args) {
    let Y = (g) => g(() => Y(g));
    return Y(f)(...args);
}

where we have Ynormal(singleDigitF, 123234234) == 0.

Now comes the trick. Since we have factored out the recursion to the Y combinator, we can count the number of recursions within it:

function Ycount(f, ...args) {
    let count = 1;
    let Y = (g) => g(() => {count += 1; return Y(g);});
    return [Y(f)(...args), count];
}

A quick check in the Node REPL gives:

> Ycount(singleDigitF, 123234234)
[ 0, 3 ]
> let digitProduct = (n) => [...(n + '')].reduce((x, y) => x * y, 1)
undefined
> digitProduct(123234234)
3456
> digitProduct(3456)
360
> digitProduct(360)
0
> Ycount(singleDigitF, 39)
[ 4, 3 ]

This combinator will now work for counting the number of calls in any recursive function written in the style of singleDigitF.

(Note that there's two sources of getting zero as a very frequent answer: numeric overflow (123345456999999999 becoming 123345457000000000 etc.), and the fact that you will almost surely get zero as an intermediate value somewhere, when the size of the input is growing.)


The traditional solution is to pass the count as a parameter to the function as suggested by another answer.

However, there is another solution in js. A few other answers suggested simply declaring count outside the recursive function:

let counter = 0
function singleDigit(num) {
  counter++;
  // ..
}

This of course works. However this makes the function non-reentrant (cannot be called twice correctly). In some cases you can ignore this problem and simply make sure you don't call singleDigit twice (javascript is single threaded so it's not too hard to do) but this is a bug waiting to happen if you update singleDigit later to be asynchronous and it also feels ugly.

The solution is to declare the counter variable outside but not globally. This is possible because javascript has closures:

function singleDigit(num) {
  let counter = 0; // outside but in a closure

  // use an inner function as the real recursive function:
  function recursion (num) {
    counter ++
    let number = [...num + ''].map(Number).reduce((x, y) => {return x * y})

    if(number <= 9){
      return counter            // return final count (terminate)
    }else{
      return recursion(number)  // recurse!
    }
  }

  return recursion(num); // start recursion
}

This is similar to the global solution but each time you call singleDigit (which is now not a recursive function) it will create a new instance of the counter variable.


Another approach, since you produce all the numbers, is to use a generator.

The last element is your number n reduced to a single digit number and to count how many times you have iterated, just read the length of the array.

const digits = [...to_single_digit(39)];
console.log(digits);
//=> [27, 14, 4]
<script>
function* to_single_digit(n) {
  do {
    n = [...String(n)].reduce((x, y) => x * y);
    yield n;
  } while (n > 9);
}
</script>

Final thoughts

You may want to consider having a return-early condition in your function. Any numbers with a zero in it will return zero.

singleDigit(1024);       //=> 0
singleDigit(9876543210); //=> 0

// possible solution: String(n).includes('0')

The same can be said for any numbers made of 1 only.

singleDigit(11);    //=> 1
singleDigit(111);   //=> 1
singleDigit(11111); //=> 1

// possible solution: [...String(n)].every(n => n === '1')

Finally, you didn't clarify whether you accept only positive integers. If you accept negative integers then casting them to strings can be risky:

[...String(39)].reduce((x, y) => x * y)
//=> 27

[...String(-39)].reduce((x, y) => x * y)
//=> NaN

Possible solution:

const mult = n =>
  [...String(Math.abs(n))].reduce((x, y) => x * y, n < 0 ? -1 : 1)

mult(39)
//=> 27

mult(-39)
//=> -27

You should add a counter argument to your function definition:

function singleDigit(num, counter = 0) {
    console.log(`called ${counter} times`)
    //...
    return singleDigit(number, counter+1)
}
singleDigit(39)