Factoring an integer looks like a small programming exercise: try some divisors, collect the ones that work, and print the result. The arithmetic is simple. The engineering is not.
A useful factor calculator must distinguish two related outputs, reject inputs that JavaScript cannot represent exactly, handle mathematical edge cases deliberately, and remain fast enough for ordinary interactive use. This article develops such an implementation from first principles.
Two questions that sound like one
“Find the factors of n” may mean either:
- enumerate every positive divisor, usually grouped into pairs; or
- express
nas a product of primes.
For 84, the factor pairs are:
1 × 84
2 × 42
3 × 28
4 × 21
6 × 14
7 × 12
Its prime factorization is:
84 = 2² × 3 × 7
These outputs require slightly different loops, but both rely on the same observation: if d divides n, then n / d is the matching divisor. At least one member of that pair is no greater than sqrt(n). We therefore never need to search beyond the square root.
This is also the basis of trial division, a standard elementary factorization method. For a concise mathematical definition of prime factorization, see Wolfram MathWorld.
Validate before calculating
JavaScript's number type uses IEEE-754 floating-point representation. Integers are represented exactly only through Number.MAX_SAFE_INTEGER. Beyond that point, distinct mathematical integers can collapse to the same runtime value. MDN's Number.isSafeInteger() documentation explains why this matters.
For a calculator backed by number, a defensible input contract is:
- accept one positive decimal integer;
- reject signs, decimal points, exponents, separators, and surrounding junk;
- reject zero and negative values;
- reject values above the safe-integer limit.
Validate the original string before converting it. Calling Number() first is too permissive: it accepts forms such as 1e3, whitespace-only strings, and hexadecimal notation.
function parsePositiveSafeInteger(raw) {
const text = String(raw).trim();
if (!/^[1-9]\d*$/.test(text)) {
throw new TypeError("Enter a positive integer in decimal notation.");
}
const value = Number(text);
if (!Number.isSafeInteger(value)) {
throw new RangeError("The integer is outside JavaScript's safe range.");
}
return value;
}
If arbitrary-size values are a real requirement, use BigInt consistently rather than silently accepting imprecise numbers. MDN provides a useful overview of BigInt, including its incompatibility with ordinary number arithmetic.
Prime factorization by trial division
The simplest robust optimization is to remove factors of two first, then test only odd candidates. The upper bound should follow the shrinking remainder, not the original input.
function primeFactors(n) {
if (!Number.isSafeInteger(n) || n < 1) {
throw new RangeError("n must be a positive safe integer");
}
if (n === 1) return [];
const result = [];
while (n % 2 === 0) {
result.push(2);
n /= 2;
}
for (let divisor = 3; divisor * divisor <= n; divisor += 2) {
while (n % divisor === 0) {
result.push(divisor);
n /= divisor;
}
}
if (n > 1) result.push(n);
return result;
}
Why is the final remainder prime? If it were composite, it would have a factor no greater than its square root, and the loop would already have removed that factor.
The worst case is a prime input, for which the loop tests candidates through sqrt(n). The running time is therefore O(sqrt(n)), while the stored factor list uses O(log n) space in the multiplicity-heavy case. Replacing the odd-number loop with a precomputed prime list can reduce modulus operations, but it does not change the basic square-root bound for this approach.
Enumerating factor pairs
Factor-pair enumeration starts at one and keeps the complementary quotient. A perfect square needs special treatment so that its square root is not duplicated.
function factorPairs(n) {
if (!Number.isSafeInteger(n) || n < 1) {
throw new RangeError("n must be a positive safe integer");
}
const pairs = [];
for (let divisor = 1; divisor * divisor <= n; divisor += 1) {
if (n % divisor !== 0) continue;
const complement = n / divisor;
pairs.push(
divisor === complement ? [divisor] : [divisor, complement]
);
}
return pairs;
}
This loop is also O(sqrt(n)). In languages with fixed-width integer overflow, prefer divisor <= Math.floor(n / divisor) to divisor * divisor <= n. JavaScript safe integers leave enough room for the loop's relevant square, but the division form can still make the invariant clearer.
Edge cases should be product decisions
Several inputs deserve explicit behavior rather than accidental output:
-
n = 1: one has the divisor list[1], but no prime factors. Returning an empty prime-factor array is more precise than displaying1as prime. -
Prime input:
97should produce[97]and the pair[1, 97]. -
Perfect square:
144must include[12]exactly once; its prime factors are2, 2, 2, 2, 3, 3. -
Repeated factors:
64should preserve multiplicity as six copies of2, not merely report that2divides it. - Large composite: correctness must not depend on the input being small or having an early odd factor.
For a visual check of these behaviors, the interactive Factor Calculator is useful as a demo alongside the code and tests, not as a substitute for them.
Reproducible tests
Example-based tests cover the semantic boundaries, while one invariant catches many ordinary mistakes: multiplying every returned prime factor must reconstruct the input.
import assert from "node:assert/strict";
assert.deepEqual(primeFactors(1), []);
assert.deepEqual(primeFactors(97), [97]);
assert.deepEqual(primeFactors(84), [2, 2, 3, 7]);
assert.deepEqual(primeFactors(144), [2, 2, 2, 2, 3, 3]);
assert.deepEqual(factorPairs(1), [[1]]);
assert.deepEqual(factorPairs(97), [[1, 97]]);
assert.deepEqual(factorPairs(144).at(-1), [12]);
const largeComposite = 61_917_364_224; // 2^20 × 3^10
const factors = primeFactors(largeComposite);
assert.equal(factors.reduce((product, x) => product * x, 1), largeComposite);
assert.equal(factors.filter(x => x === 2).length, 20);
assert.equal(factors.filter(x => x === 3).length, 10);
assert.throws(() => parsePositiveSafeInteger("0"));
assert.throws(() => parsePositiveSafeInteger("12.5"));
assert.throws(() => parsePositiveSafeInteger("1e3"));
assert.throws(() => parsePositiveSafeInteger("9007199254740992"));
Additional property tests can verify that every pair multiplies to n, every reported prime factor is at least two, and the flattened factorization remains sorted. These properties separate mathematical correctness from presentation details such as superscripts or comma-separated output.
Keep the contract visible
Trial division is not the fastest known factoring algorithm, but it is transparent, easy to audit, and entirely adequate for bounded interactive inputs. The most important design choice is not a micro-optimization. It is publishing an honest input contract, validating it before conversion, and testing the values where mathematical definitions and software representations meet.
Top comments (0)