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
    Mar 19, 2016 at 0:27
  • \$\begingroup\$ @Downgoat Yup, good catches. I've fixed my answer. \$\endgroup\$
    – SirPython
    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\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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