Numeric Aggregations: Sum, Filter, Closest Pair
Three numeric questions you will hit again and again on real arrays: sum (with safety against non-number inputs), summary statistics (sum, average, min, max in one pass), and the smallest absolute difference between any two numbers. Each one is short, but the right answer changes with the input shape: mixed-type arrays, large arrays, and "closest pair" all reward different idioms.
1,071 views
6
// Plain sum: trusts that every item is a finite number.
const sum = (arr) => arr.reduce((acc, n) => acc + n, 0);
console.log(sum([1, 2, 3, 4, 5])); // 15
// Coerced + filtered: ignore strings, undefined, null, NaN, Infinity.
const sumNumeric = (arr) =>
arr.reduce((acc, x) => acc + (Number.isFinite(x) ? x : 0), 0);
console.log(sumNumeric([1, 'two', 3, null, 5, NaN, 7])); // 16
console.log(sumNumeric([10, '20', 30])); // 40 (string '20' rejected)
// If you DO want to coerce numeric strings, parse first.
const sumLoose = (arr) =>
arr.reduce((acc, x) => {
const n = Number(x);
return acc + (Number.isFinite(n) ? n : 0);
}, 0);
console.log(sumLoose([10, '20', 30, 'x'])); // 60reduce((acc, n) => acc + n, 0) is the textbook sum and is correct as long as the input is well-typed. In real data (CSV imports, JSON from a third party), you often get strings, null, or NaN mixed in, and the plain sum returns NaN from then on. Number.isFinite(x) rejects those without coercing strings, so the safe form picks up only real finite numbers. Choose between strict (reject strings outright) and loose (parse strings via Number(x)) based on whether numeric strings are a valid input in your data contract; do not silently accept either form when you have not decided.
// Walk the array once and produce a summary object.
const summarize = (arr) => {
if (arr.length === 0) {
return { count: 0, sum: 0, avg: NaN, min: NaN, max: NaN };
}
let sum = 0;
let min = Infinity;
let max = -Infinity;
for (const n of arr) {
if (!Number.isFinite(n)) continue;
sum += n;
if (n < min) min = n;
if (n > max) max = n;
}
return { count: arr.length, sum, avg: sum / arr.length, min, max };
};
console.log(summarize([3, 1, 4, 1, 5, 9, 2, 6]));
// { count: 8, sum: 31, avg: 3.875, min: 1, max: 9 }
console.log(summarize([]));
// { count: 0, sum: 0, avg: NaN, min: NaN, max: NaN }Calling Math.min(...arr), Math.max(...arr), and a separate reduce for the sum walks the array three times and the spread risks a stack overflow for very large arrays (the engine spreads each element as a function argument). A single for...of pass computes everything in one read, which is both faster and safer for large inputs. The empty-array sentinel returns NaN for avg, min, and max because there is nothing to average or compare; tweak that to throw or return null based on what your callers expect.
// Brute force is O(n^2). Sort + walk adjacents is O(n log n).
const closestPairDiff = (arr) => {
if (arr.length < 2) return Infinity;
const sorted = [...arr].sort((a, b) => a - b);
let best = Infinity;
let bestPair = null;
for (let i = 1; i < sorted.length; i++) {
const diff = sorted[i] - sorted[i - 1];
if (diff < best) {
best = diff;
bestPair = [sorted[i - 1], sorted[i]];
}
}
return { diff: best, pair: bestPair };
};
console.log(closestPairDiff([1, 5, 3, 8, 12]));
// { diff: 2, pair: [1, 3] }
console.log(closestPairDiff([10, 100, 50, 49.5]));
// { diff: 0.5, pair: [49.5, 50] }
console.log(closestPairDiff([7]));
// InfinityThe naive answer compares every pair (O(n^2)) which is fine on tiny inputs but slow on anything moderate. The trick is that the closest pair must be adjacent in the sorted order: any non-adjacent pair has at least one item between them, and that item is closer to one of the two by definition. So sort once (O(n log n)) and walk consecutive neighbors. Spread ([...arr]) before sorting so the input is not mutated. The function returns both the difference and the actual pair, which is more useful at the call site than just the number; trim it to just the diff if that is what you need.
