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.)