3
\$\begingroup\$

I posted a similar question “Curious Numbers” (HackerRank PE 34) recently (but in a different language/platform altogether). I'm starting to learn JavaScript and decided to take the challenge again and adapt it to this language's features and quirks.

While I could have written many of the functions below using for-loops, I decided instead to focus on getting better with the FP aspects of JS, in particular map, reduce, filter and apply.

I think it is pretty fast, with the longest of the 5 challenges on HackerRank executing in 2.28s for a calculation in the neighborhood of \$10^5\$, but I'm certainly open to ways to make it faster, cleaner, or better in any other way.

\$19!\$ is a curious number, as \$1!+9!=1+362880=362881\$ is divisible by \$19\$.

Find the sum of all numbers below \$N\$ which divide the sum of the factorial of their digits. Note: as \$1!,2!,\cdots,9!\$ are not sums, so they are not included.

Input Format: Input contains an integer \$N\$

Output Format: Print the answer corresponding to the test case.

Constraints: \$10^1 \le N \le 10^5\$

Sample Input

20

Sample Output

19
// HackerRank Project Euler #34: Digit factorials
// https://www.hackerrank.com/contests/projecteuler/challenges/euler034

function isStrictInt(input) {
    // Confirms whether input is 1) of number type, 2) not equal to the NaN constant, and 3) can be parsed to an integer.
    // Reference: http://stackoverflow.com/a/29658971/3626537
    return typeof input === "number" 
            && !isNaN(input)
            && parseInt(input) === input;
}

function arrayOfNCopies(value, N) {
    // Makes an array of N copies of value.
    if (!isStrictInt(N)) {
        return NaN;
    } 
    else if (N === 0) { 
        return []; 
    } 
    return Array(Math.abs(N) + 1).join(value).split("");
}

function arrayOfNConsecutiveInts(N) {
    // Makes an array of consecutive integers from 1 to N.
    // Ex: arrayOfNConsecutiveInts(5) => [1,2,3,4,5]
    if (!isStrictInt(N)) {
        return NaN;
    } 
    else if (N === 0) { 
        return []; 
    } 
    return Array
        // makes array of N size and assigns `undefined` to each index (to avoid nulls)
        .apply(null, Array(N))
        .map(function (a, b) { return b + 1; });
}

function powerOf(base, exponent) {
    // Calculates an exponential (i.e. B^X or "B to the power of X").
    // Ex: powerOf(2,3) => 2 * 2 * 2 => 8
    if (!isStrictInt(base) || !isStrictInt(exponent)) {
        return NaN;
    }
    else if (exponent === 0) { 
        return 1; 
    }
    else if (base === 0) { 
        return 0; 
    }
    else {
        var result = arrayOfNCopies(base, exponent).reduce(function(a, b) { return a * b; });
        if (exponent < 0) {
            return 1 / result;
        }
        return result;
    } 
}

function factorial(N) {
    // Calculates N! (i.e. N factorial).
    // Ex: factorial(4) => 4 * 3 * 2 * 1 => 24
    if (!isStrictInt(N)) {
        return NaN;
    }
    // Negative numbers' factorials are mathematically undefined:
    else if (N < 0) {
        return NaN;
    }
    else if (N === 0) { 
        return 1; 
    }
    return arrayOfNConsecutiveInts(N).reduce(function (a, b) { 
        return a * b; 
    });
}

function explodeIntToDigits(N) {
    // Given a number N, decompose it into an array of its digits.
    // Ex: explodeIntToDigits(1234) => [1, 2, 3, 4]
    if (!isStrictInt(N)) {
        return NaN;
    }
    return N.toString(10).split("").map(function (t) { 
        return parseInt(t); 
    });
}

function sumOfFactorialOfDigits(N) {
    // Given a number N, returns the sum of the factorials of each digit of N.
    // Ex: sumOfFactorialOfDigits(35) => 3! + 5! => 6 + 120 => 126
    if (!isStrictInt(N)) {
        return NaN;
    }
    return explodeIntToDigits(N).map(function (t) { 
        return factorial(t); 
    }).reduce(function (a, b) { 
        return a + b; 
    });
}


function isCuriousNumber(N) {
    // A 'Curious Number' is a number where the sum of the factorial of each of its digits is evenly divisible by the number itself.
    // Ex: isCuriousNumber(19) => true, because: 1! + 9! = 1 + 362880 = 362881, and: 362881 % 19 = 0
    if (!isStrictInt(N)) {
        return NaN;
    }
    else if (sumOfFactorialOfDigits(N) % N !== 0) {
        return false;
    }
    return true;
}

function sumAllCuriousNumbersUpTo(N) {
    // Given a number N up to 10^5, return the sum of a list of all 'Curious Numbers' 10 to N inclusive.
    // This is as per constraint: 10 ≤ N ≤ 10^5
    if (!isStrictInt(N)) {
        return NaN;
    }
    return arrayOfNConsecutiveInts(N).filter(function (t) { 
        return t >= 10 && isCuriousNumber(t); 
    }).reduce(function (a, b) { 
        return a + b; 
    }, 0);
}
\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Your code looks very nice.


Built-ins

Your powerOf function is mimicking Math.pow exactly. Use the built-in instead.

This also means you won't need arrayOfNCopies now.


Consistent return values

You've done a good job doing type checking so your functions don't receive invalid inputs. But, sometimes your return values don't make sense (I only found one case).

Here: your arrayOfNConsecutiveInts returns NaN if N isn't a valid number, but an entirely different type - an array - if it is valid.

It's best to be consistent with your return types. Here, it might be best to throw an error.

I'm not 100% sure about this.


Refactor array summing

This construct

.reduce(function (a, b) { 
    return a + b; 
});

has a few flaws, in my opinion:

  • It's not very understandable at first glace (at least in my opinion, but I'm sure others will disagree).
  • You repeat it a lot.

Also, while I'm not that familiar with it, is it still good practice in FP when one of a method's inputs is the object itself?

Either way, this could refactored to a separate function:

function sumArray(arr) {
    ... type checking...
    arr.reduce(function(a, b) {
        return a + b;
    });
}

Or, if you use ES6 in the future, it could look even cleaner:

let sumArray = (arr) => arr.reduce((a, b) => a + b);

Nitpicks

  • Always supply parseInt with its second radix parameter.
  • This:

    else if (sumOfFactorialOfDigits(N) % N !== 0) {
        return false;
    }
    return true;
    

can be simplified to:

return sumOfFactorialOfDigits(N) % N === 0
\$\endgroup\$
2
  • \$\begingroup\$ doesn't !(sumOfFactorialOfDigits(N) % N !== 0) == sumOfFactorialOfDigits(N) % N === 0? Also in your sumArray(arr) function. You're missing a closing ) on the reduce \$\endgroup\$
    – Downgoat
    Commented Mar 19, 2016 at 0:27
  • \$\begingroup\$ @Downgoat Yup, good catches. I've fixed my answer. \$\endgroup\$
    – SirPython
    Commented Mar 19, 2016 at 13:32
2
\$\begingroup\$

Refactored & improved code

I have applied suggestions from @SirPython and some others and refactored it significantly.

  • Eliminated powerOf function and its dependent arrayOfNCopies. After looking through the code I noticed I was not even using powerOf once, so I didn't need to use the built-in Math.pow after all.
  • Made functions that would normally return an array to throw a TypeError if the parameter(s) supplied cannot be used as such.
  • Extracted the repeated reduce logic to sumArray and miltiplyArray functions.
  • Added radix parameter to parseInt calls.
  • Made return of isCuriousNumber more concise.

// HackerRank Project Euler #34: Digit factorials
// https://www.hackerrank.com/contests/projecteuler/challenges/euler034

function isStrictInt(input) {
    // Confirms whether input is 1) of number type, 2) not equal to the NaN constant, and 3) can be parsed to an integer.
    // Reference: http://stackoverflow.com/a/29658971/3626537
    return typeof input === "number" 
            && !isNaN(input)
            && parseInt(input, 10) === input;
}

function arrayOfNConsecutiveInts(N) {
    // Makes an array of consecutive integers from 1 to N.
    // Ex: arrayOfNConsecutiveInts(5) => [1,2,3,4,5]
    if (!isStrictInt(N)) {
        throw new TypeError("cannot parse " + N + " into an array of consecutive integers.");
    } 
    else if (N === 0) { 
        return []; 
    } 
    return Array
        // makes array of N size and assigns `undefined` to each index (to avoid nulls)
        .apply(null, Array(N))
        .map(function (a, b) { return b + 1; });
}

function sumArray(arr) {
    if (!arr.reduce) {
        throw new TypeError("Invalid argument `" + arr + "` to sumArray().");
    }
    return arr.reduce(function (curr, next) {
        return curr + next;
    }, 0);
}
function multiplyArray(arr) {
    if (!arr.reduce) {
        throw new TypeError("Invalid argument `" + arr + "` to multiplyArray().");
    }
    return arr.reduce(function (curr, next) {
        return curr * next;
    }, 1);
}

function factorial(N) {
    // Calculates N! (i.e. N factorial).
    // Ex: factorial(4) => 4 * 3 * 2 * 1 => 24
    if (!isStrictInt(N)) {
        return NaN;
    }
    // Negative numbers' factorials are mathematically undefined:
    else if (N < 0) {
        return NaN;
    }
    else if (N === 0) { 
        return 1; 
    }
    return multiplyArray(arrayOfNConsecutiveInts(N));
}

function explodeIntToDigits(N) {
    // Given a number N, decompose it into an array of its digits.
    // Ex: explodeIntToDigits(1234) => [1, 2, 3, 4]
    if (!isStrictInt(N)) {
        throw new TypeError("cannot parse " + N + " into an array of digits.");
    }
    return N.toString(10).split("").map(function (t) { 
        return parseInt(t, 10); 
    });
}

function sumOfFactorialOfDigits(N) {
    // Given a number N, returns the sum of the factorials of each digit of N.
    // Ex: sumOfFactorialOfDigits(35) => 3! + 5! => 6 + 120 => 126
    if (!isStrictInt(N)) {
        return NaN;
    }
    return sumArray(explodeIntToDigits(N).map(factorial));
}


function isCuriousNumber(N) {
    // A 'Curious Number' is a number where the sum of the factorial of each of its digits is evenly divisible by the number itself.
    // Ex: isCuriousNumber(19) => true, because: 1! + 9! = 1 + 362880 = 362881, and: 362881 % 19 = 0
    if (!isStrictInt(N)) {
        return NaN;
    }
    return !(sumOfFactorialOfDigits(N) % N !== 0);
}

function sumAllCuriousNumbersUpTo(N) {
    // Given a number N up to 10^5, return the sum of a list of all 'Curious Numbers' 10 to N inclusive.
    // This is as per constraint: 10 ≤ N ≤ 10^5
    if (!isStrictInt(N)) {
        return NaN;
    }
    return sumArray(arrayOfNConsecutiveInts(N).filter(function (t) { 
        return t >= 10 && isCuriousNumber(t); 
    }));
}
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.