Mike Chen
Mike Chen

Reputation: 2608

Dividing an array by filter function

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

Answers (17)

Nick F
Nick F

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

Hao Wu
Hao Wu

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

moult86
moult86

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

Yaremenko Andrii
Yaremenko Andrii

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

nkitku
nkitku

Reputation: 5738

ONE-LINER Partition

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));

For Typescript (v4.5)

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

nart
nart

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

Sebastian
Sebastian

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

Andreas Gassmann
Andreas Gassmann

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

Vereb
Vereb

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

parktomatomi
parktomatomi

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

kazuwombat
kazuwombat

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

UDrake
UDrake

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

braza
braza

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

Zoltan Kochan
Zoltan Kochan

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']]

or ramda.partition

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

Brandan
Brandan

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

Setthase
Setthase

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

qwertymk
qwertymk

Reputation: 35294

Try this:

function filter(a, fun) {
    var ret = { good: [], bad: [] };
    for (var i = 0; i < a.length; i++)
        if (fun(a[i])
            ret.good.push(a[i]);
        else
            ret.bad.push(a[i]);
    return ret;
}

DEMO

Upvotes: 6

Related Questions