Reputation: 2608
I have a Javascript array that I would like to split into two based on whether a function called on each element returns true
or false
. Essentially, this is an array.filter
, but I'd like to also have on hand the elements that were filtered out.
Currently, my plan is to use array.forEach
and call the predicate function on each element. Depending on whether this is true or false, I will push the current element onto one of the two new arrays. Is there a more elegant or otherwise better way to do this? An array.filter
where the will push the element onto another array before it returns false
, for instance?
Upvotes: 185
Views: 112365
Reputation: 10122
JavaScript recently introduced Sets which provide an alternative way of doing this*.
Sets have methods like difference
and intersection
that are specifically designed for this use case: that is, to create a new set based on difference between or overlap with another set.
Here's an example of how you might approach this using Sets:
const isEven = n => n % 2 === 0;
let x = new Set([1,2,3,4,5,6]);
let even = new Set(Array.from(x).filter(n => isEven(n)));
let odd = x.difference(even);
We create the even
set by filtering our original set x
, but then we can create the odd
set by getting the difference: x.difference(even)
returns everything that is in x
but not in even
.
Is this "better" than writing a partition function, as suggested by other answers? Not necessarily - for one thing, the need to convert to an array in order to filter and then back to a Set
is a bit awkward - but it's good to know about and might be worth considering depending on your use case.
* One important thing to note is that Sets, unlike arrays, do not allow for duplicate values (they de-duplicate repeats of the same value), so if your array might have duplicate values (and you need to preserve these duplicates in the output) then Set composition/comparison methods are probably not appropriate for your particular use case.
Upvotes: 0
Reputation: 20867
It can be done using Object.groupBy
. Imagine if I want to separate an array by whether its elements are larger than 2.
const array = [1, 2, 3, 4, 5];
const { true: large, false: small } = Object.groupBy(array, e => e > 2);
console.log({ large, small });
Be aware of its coverage. By March 2024, the overall coverage of this built-in function is below 90% and is not advised to use it on production environments.
Upvotes: 8
Reputation: 73
Most of the answers here are simply variations of Array.reduce, which is a correct way to accomplish this. I wrote a small library that abstracts all this away with a simple API for partitioning arrays in JavaScript. Additionally it has the ability to spawn a web worker (or web thread in Node) to make reducing arrays to partitions async.
const someArray = [1, ... , 100]
console.log('--- start ---');
const reduced = someArray.reduce((reducer, i) => {
// Different variations of fancy code can go here that all do the same thing
if (i < 33) reducer[0].push(i)
else if (i => i > 32 && i < 66) reducer[1].push(i)
else if (i => i > 67) reducer[2].push(i)
return reducer
}, [[], [], []])
console.log('array.reduce done processing');
console.log('--- UI element loaded ---');
'--- start ---'
'array.reduce done processing'
'--- UI element loaded ---'
Here is how you would accomplish the same thing with the library but async
const someArray = [1, ... , 100]
console.log('--- start ---');
partition()
.async()
.add(i => i < 33)
.add(i => i > 32 && i < 66)
.add(i => i > 67)
.split(someArray)
.then(result => {
console.log('Partitions done processing');
});
console.log('--- UI element loaded ---');
'--- start ---'
'--- UI element loaded ---'
'Partitions done processing'
The output of both would be three array paritions.
[
[1, ..., 32],
[33, ..., 65],
[66, ..., 100]
]
Upvotes: 1
Reputation: 799
You can use reduce for it:
function partition(array, callback){
return array.reduce(function(result, element, i) {
callback(element, i, array)
? result[0].push(element)
: result[1].push(element);
return result;
}, [[],[]]
);
};
Or if using Typescript:
const partition = <T,>(
array: T[],
callback: (element: T, index: number, array: T[]) => boolean
) => {
return array.reduce(function(result, element, i) {
callback(element, i, array)
? result[0].push(element)
: result[1].push(element);
return result;
}, [[],[]]);
};
Example:
const groceries = [
{ type: "apple" },
{ type: "pear" },
{ type: "banana" }
]
const [apples, others] = partition(
groceries,
(item) => item.type === "apple",
);
// => apples: [{ type: "apple" }]
// => others: [{ type: "pear" }, { type: "banana" }]
Using ES6 syntax you also can do that using recursion (updated to avoid creating new arrays on every iteration):
function partition([current, ...tail], f, left = [], right = []) {
if(current === undefined) {
return [left, right];
}
if(f(current)) {
left.push(current);
return partition(tail, f, left, right);
}
right.push(current);
return partition(tail, f, left, right);
}
Upvotes: 31
Reputation: 5738
const partitionBy = (arr, predicate) =>
arr.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
DEMO
// to make it consistent to filter pass index and array as arguments
const partitionBy = (arr, predicate) =>
arr.reduce(
(acc, item, index, array) => (
acc[+!predicate(item, index, array)].push(item), acc
),
[[], []]
);
console.log(partitionBy([1, 2, 3, 4, 5], x => x % 2 === 0));
console.log(partitionBy([..."ABCD"], (x, i) => i % 2 === 0));
const partitionBy = <T>(
arr: T[],
predicate: (v: T, i: number, ar: T[]) => boolean
) =>
arr.reduce(
(acc, item, index, array) => {
acc[+!predicate(item, index, array)].push(item);
return acc;
},
[[], []] as [T[], T[]]
);
Upvotes: 2
Reputation: 1858
Lodash partition alternative, same as the first solution of @Yaremenko Andrii but shorter syntax
function partition(arr, callback) {
return arr.reduce(
(acc, val, i, arr) => {
acc[callback(val, i, arr) ? 0 : 1].push(val)
return acc
},
[[], []]
)
}
Upvotes: 1
Reputation: 118
I know there are multiple solutions already but I took the liberty of putting together the best bits of the answers above and used extension methods on Typescript. Copy and paste and it just works:
declare global {
interface Array<T> {
partition(this: T[], predicate: (e: T) => boolean): T[][];
}
}
if(!Array.prototype.partition){
Array.prototype.partition = function<T>(this: T[], predicate: (e: T) => boolean): T[][] {
return this.reduce<T[][]>(([pass, fail], elem) => {
(predicate(elem) ? pass : fail).push(elem);
return [pass, fail];
}, [[], []]);
}
}
Usage:
const numbers = [1, 2, 3, 4, 5, 6];
const [even, odd] = numbers.partition(n => n % 2 === 0);
Upvotes: -1
Reputation: 6544
I ended up doing this because it's easy to understand:
const partition = (array, isValid) => {
const pass = []
const fail = []
array.forEach(element => {
if (isValid(element)) {
pass.push(element)
} else {
fail.push(element)
}
})
return [pass, fail]
}
// usage
const [pass, fail] = partition([1, 2, 3, 4, 5], (element) => element > 3)
And the same method including types for typescript:
const partition = <T>(array: T[], isValid: (element: T) => boolean): [T[], T[]] => {
const pass: T[] = []
const fail: T[] = []
array.forEach(element => {
if (isValid(element)) {
pass.push(element)
} else {
fail.push(element)
}
})
return [pass, fail]
}
// usage
const [pass, fail] = partition([1, 2, 3, 4, 5], (element: number) => element > 3)
Upvotes: 5
Reputation: 14726
What about this?
[1,4,3,5,3,2].reduce( (s, x) => { s[ x > 3 ].push(x); return s;} , {true: [], false:[]} )
Probably this is more efficient than the spread operator
Or a bit shorter, but uglier
[1,4,3,5,3,2].reduce( (s, x) => s[ x > 3 ].push(x)?s:s , {true: [], false:[]} )
Upvotes: 10
Reputation: 4089
A lot of answers here use Array.prototype.reduce
to build a mutable accumulator, and rightfully point out that for large arrays, this is more efficient than, say, using a spread operator to copy a new array each iteration. The downside is that it's not as pretty as a "pure" expression using the short lambda syntax.
But a way around that is to use the comma operator. In C-like languages, comma is an operator that always returns the right hand operand. You can use this to create an expression that calls a void function and returns a value.
function partition(array, predicate) {
return array.reduce((acc, item) => predicate(item)
? (acc[0].push(item), acc)
: (acc[1].push(item), acc), [[], []]);
}
If you take advantage of the fact that a boolean expression implicitly casts to a number as 0 and 1, and you can make it even more concise, although I don't think it's as readable:
function partition(array, predicate) {
return array.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
}
Usage:
const [trues, falses] = partition(['aardvark', 'cat', 'apple'], i => i.startsWith('a'));
console.log(trues); // ['aardvark', 'apple']
console.log(falses); // ['cat']
Upvotes: 12
Reputation: 1673
Easy to read one.
const partition = (arr, condition) => {
const trues = arr.filter(el => condition(el));
const falses = arr.filter(el => !condition(el));
return [trues, falses];
};
// sample usage
const nums = [1,2,3,4,5,6,7]
const [evens, odds] = partition(nums, (el) => el%2 == 0)
Upvotes: 6
Reputation: 530
I came up with this little guy. It uses for each and all that like you described, but it looks clean and succinct in my opinion.
//Partition function
function partition(array, filter) {
let pass = [], fail = [];
array.forEach((e, idx, arr) => (filter(e, idx, arr) ? pass : fail).push(e));
return [pass, fail];
}
//Run it with some dummy data and filter
const [lessThan5, greaterThanEqual5] = partition([0,1,4,3,5,7,9,2,4,6,8,9,0,1,2,4,6], e => e < 5);
//Output
console.log(lessThan5);
console.log(greaterThanEqual5);
Upvotes: 49
Reputation: 4672
With ES6 you can make use of the spread syntax with reduce:
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
}, [[], []]);
}
const [pass, fail] = partition(myArray, (e) => e > 5);
Or on a single line:
const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
Upvotes: 129
Reputation: 7676
You can use lodash.partition
var users = [
{ 'user': 'barney', 'age': 36, 'active': false },
{ 'user': 'fred', 'age': 40, 'active': true },
{ 'user': 'pebbles', 'age': 1, 'active': false }
];
_.partition(users, function(o) { return o.active; });
// → objects for [['fred'], ['barney', 'pebbles']]
// The `_.matches` iteratee shorthand.
_.partition(users, { 'age': 1, 'active': false });
// → objects for [['pebbles'], ['barney', 'fred']]
// The `_.matchesProperty` iteratee shorthand.
_.partition(users, ['active', false]);
// → objects for [['barney', 'pebbles'], ['fred']]
// The `_.property` iteratee shorthand.
_.partition(users, 'active');
// → objects for [['fred'], ['barney', 'pebbles']]
R.partition(R.contains('s'), ['sss', 'ttt', 'foo', 'bars']);
// => [ [ 'sss', 'bars' ], [ 'ttt', 'foo' ] ]
R.partition(R.contains('s'), { a: 'sss', b: 'ttt', foo: 'bars' });
// => [ { a: 'sss', foo: 'bars' }, { b: 'ttt' } ]
Upvotes: 76
Reputation: 14983
This sounds very similar to Ruby's Enumerable#partition
method.
If the function can't have side-effects (i.e., it can't alter the original array), then there's no more efficient way to partition the array than iterating over each element and pushing the element to one of your two arrays.
That being said, it's arguably more "elegant" to create a method on Array
to perform this function. In this example, the filter function is executed in the context of the original array (i.e., this
will be the original array), and it receives the element and the index of the element as arguments (similar to jQuery's each
method):
Array.prototype.partition = function (f){
var matched = [],
unmatched = [],
i = 0,
j = this.length;
for (; i < j; i++){
(f.call(this, this[i], i) ? matched : unmatched).push(this[i]);
}
return [matched, unmatched];
};
console.log([1, 2, 3, 4, 5].partition(function (n, i){
return n % 2 == 0;
}));
//=> [ [ 2, 4 ], [ 1, 3, 5 ] ]
Upvotes: 15
Reputation: 14418
In filter function you can push your false items into another variable outside function:
var bad = [], good = [1,2,3,4,5];
good = good.filter(function (value) { if (value === false) { bad.push(value) } else { return true});
Of course value === false
need to be real comparasion ;)
But it do almost that same operation like forEach
. I think you should use forEach
for better code readability.
Upvotes: 16