Partition an Array by a Predicate
Calling `array.filter(p)` and `array.filter((x) => !p(x))` works but walks the input twice and runs the predicate twice per element, which is wasteful and (for non-pure predicates) plain wrong. A single-pass `partition` returns the matched and unmatched buckets in one go. This snippet covers a clean fold-based implementation, an N-way `partitionBy` for multi-class splits, and a streaming variant that lazily partitions an iterable without materialising the full input.
758 views
18
function partition(array, predicate) {
const pass = [];
const fail = [];
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (predicate(item, i, array)) {
pass.push(item);
} else {
fail.push(item);
}
}
return [pass, fail];
}
const numbers = [1, 2, 3, 4, 5, 6];
const [evens, odds] = partition(numbers, (n) => n % 2 === 0);
console.log(evens); // [2, 4, 6]
console.log(odds); // [1, 3, 5]
const users = [
{ name: 'Ada', active: true },
{ name: 'Bo', active: false },
{ name: 'Cal', active: true },
];
const [active, inactive] = partition(users, (u) => u.active);
console.log(active.length, inactive.length); // 2 1A classic for loop runs the predicate exactly once per element and pushes into one of two buckets. Returning a tuple [pass, fail] lets the caller destructure cleanly: const [yes, no] = partition(...). Compared to [arr.filter(p), arr.filter(notP)], this version is twice as fast on large inputs and correct even when the predicate is impure (e.g. randomised, time-based, or one that mutates a counter). The third predicate argument matches the Array.prototype.filter contract (item, index, array), so any predicate that works with filter is a drop-in here.
const partitionR = (array, predicate) =>
array.reduce(
(acc, item, i) => {
acc[predicate(item, i, array) ? 0 : 1].push(item);
return acc;
},
[[], []]
);
const words = ['cat', 'banana', 'apple', 'dog', 'kiwi'];
const [short, long] = partitionR(words, (w) => w.length <= 3);
console.log(short); // ['cat', 'dog']
console.log(long); // ['banana', 'apple', 'kiwi']
// Same shape as filter, so any filter predicate works.
const signed = [-3, -1, 0, 2, 5];
const [nonNeg, neg] = partitionR(signed, (n) => n >= 0);
console.log(nonNeg); // [0, 2, 5]
console.log(neg); // [-3, -1]When the codebase prefers expression-style helpers (no statements, no temporary let), reduce produces the same result with a single traversal. The accumulator is a pre-built [[], []] tuple and the predicate decides which bucket index (0 or 1) receives the item. Performance-wise this version and the imperative one are within a few percent in V8, so pick whichever reads better in your codebase. Avoid the temptation to write acc[predicate ? "pass" : "fail"] with object keys; the array-of-arrays shape is what makes destructuring at the call site so ergonomic.
function partitionBy(array, classifyFn) {
const groups = new Map();
for (const item of array) {
const label = classifyFn(item);
const bucket = groups.get(label);
if (bucket) {
bucket.push(item);
} else {
groups.set(label, [item]);
}
}
return groups;
}
const transactions = [
{ id: 1, amount: -25 },
{ id: 2, amount: 0 },
{ id: 3, amount: 80 },
{ id: 4, amount: -5 },
{ id: 5, amount: 12 },
];
const buckets = partitionBy(transactions, (t) => {
if (t.amount < 0) return 'debit';
if (t.amount > 0) return 'credit';
return 'zero';
});
console.log(buckets.get('debit').length); // 2
console.log(buckets.get('credit').length); // 2
console.log(buckets.get('zero').length); // 1
console.log([...buckets.keys()]); // ['debit', 'zero', 'credit']A two-bucket split breaks down quickly: tri-state validation (valid / invalid / pending), HTTP status classes (2xx / 4xx / 5xx), or transaction sign (debit / zero / credit). partitionBy generalises by letting the classifier return any label and bucketing into a Map. Map keys preserve insertion order, which is why iterating later yields buckets in the order their first-seen labels appeared. This is essentially groupBy, but framing it as a partition emphasises that every item goes into exactly one bucket and the labels are typically a small fixed enum rather than free-form keys.
async function* streamRange(n) {
for (let i = 0; i < n; i++) {
// Pretend each yield is an async I/O read.
yield i;
}
}
async function partitionStream(iterable, predicate) {
const pass = [];
const fail = [];
for await (const item of iterable) {
(predicate(item) ? pass : fail).push(item);
}
return [pass, fail];
}
(async () => {
const [evens, odds] = await partitionStream(streamRange(10), (n) => n % 2 === 0);
console.log(evens); // [0, 2, 4, 6, 8]
console.log(odds); // [1, 3, 5, 7, 9]
})();Real production data often arrives as a stream: rows from a paginated API, lines of a log file, messages off a queue. Using an async iterable with for await keeps memory bounded to whatever the source yields per tick, instead of materialising the entire dataset before partitioning. The predicate stays synchronous here, but the same loop trivially supports await predicate(item) if classification needs a lookup. The trade-off is that the result is fully materialised at the end; if even the buckets are too large, switch to writing each item to two output streams instead of two arrays.
