Division d'un tableau par fonction de filtre

92

J'ai un tableau Javascript que je voudrais diviser en deux selon que la fonction appelée sur chaque élément renvoie trueou false. Pour l' essentiel, c'est un array.filter, mais je voudrais aussi avoir à portée de main les éléments qui ont été filtrés sur .

Actuellement, mon plan est d'utiliser array.forEachet d'appeler la fonction de prédicat sur chaque élément. Selon que cela est vrai ou faux, je vais pousser l'élément actuel sur l'un des deux nouveaux tableaux. Existe-t-il une manière plus élégante ou meilleure de le faire? Un array.filteroù le poussera l'élément sur un autre tableau avant son retour false, par exemple?

Mike Chen
la source
Si vous pouvez publier un exemple de code, cela vous aidera à mieux répondre!
Mark Pieszak - Trilon.io
Quelle que soit l'implémentation que vous utilisez, Javascript devra toujours: parcourir les éléments, exécuter la fonction, pousser l'élément dans un tableau. Je ne pense pas qu'il y ait moyen de rendre cela plus efficace.
TheZ
3
Vous pouvez faire ce que vous voulez dans le rappel, .filtermais ces effets secondaires sont difficiles à suivre et à comprendre. Il suffit de parcourir le tableau et de pousser vers un tableau ou un autre.
Felix Kling

Réponses:

76

Avec ES6, vous pouvez utiliser la syntaxe de diffusion avec réduire:

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

Ou sur une seule ligne:

const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
braza
la source
8
Pour moi, la partition lodash ou juste pourEach serait plus facile à comprendre, mais même ainsi, bel effort
Toni Leigh
15
Cela créera deux nouveaux tableaux pour chaque élément de l'original. Alors qu'un tableau n'aura que deux éléments, l'autre grandit avec la taille du tableau. Ce sera donc super lent et gaspillera beaucoup de mémoire. (Vous pourriez faire la même chose avec une poussée et ce serait plus efficace.)
Stuart Schechter
Merci, j'ai économisé mon temps!
7urkm3n
C'est vraiment utile, braza. Remarque pour les autres: il existe des versions plus lisibles ci-dessous.
Raffi le
38

Vous pouvez utiliser 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']]

ou 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' }  ]
Zoltan Kochan
la source
16

Vous pouvez utiliser pour cela réduire:

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;
      }, [[],[]]
    );
 };

Mise à jour. En utilisant la syntaxe ES6, vous pouvez également le faire en utilisant la récursivité:

function partition([current, ...tail], f, [left, right] = [[], []]) {
    if(current === undefined) {
        return [left, right];
    }
    if(f(current)) {
        return partition(tail, f, [[...left, current], right]);
    }
    return partition(tail, f, [left, [...right, current]]);
}
Yaremenko Andrii
la source
3
À mon humble avis, la première solution (push) est de meilleures performances à mesure que la taille du tableau augmente.
ToolmakerSteve
@ToolmakerSteve pourriez-vous expliquer pourquoi? J'ai aussi lu le commentaire sur la meilleure réponse mais je ne sais toujours pas pourquoi
buncis
2
@buncis. La première approche examine chaque élément une fois, en poussant simplement cet élément vers le tableau approprié. La seconde approche construit [...left, current]ou [...right, current]- pour chaque élément. Je ne connais pas les éléments internes exacts, mais je suis sûr que la construction coûte plus cher que de simplement pousser un élément sur un tableau. De plus, en règle générale, la récursivité est plus coûteuse que l' itération , car elle implique la création à chaque fois d'un "cadre de pile".
ToolmakerSteve le
14

Cela ressemble beaucoup à laEnumerable#partition méthode de Ruby .

Si la fonction ne peut pas avoir d'effets secondaires (c'est-à-dire qu'elle ne peut pas modifier le tableau d'origine), alors il n'y a pas de moyen plus efficace de partitionner le tableau que d'itérer sur chaque élément et de pousser l'élément vers l'un de vos deux tableaux.

Cela étant dit, il est sans doute plus «élégant» de créer une méthode Arraypour exécuter cette fonction. Dans cet exemple, la fonction de filtre est exécutée dans le contexte du tableau d'origine (c'est-à-dire, thissera le tableau d'origine), et elle reçoit l'élément et l'index de l'élément en arguments (similaire à la eachméthode de jQuery ):

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 ] ]
Brandan
la source
11
Pour le lecteur moderne, veuillez ne pas ajouter de méthodes aux objets de bibliothèque standard globaux. C'est dangereux et susceptible d'être écrasé, conduisant à un comportement mystérieux et brisé. Une vieille fonction simple, correctement portée, est beaucoup plus sûre, et appeler myFunc (array) n'est pas moins "élégant" que array.myFunc ().
Emmett R.
14

Je suis venu avec ce petit gars. Il utilise pour tout ce que vous avez décrit, mais il a l'air propre et succinct à mon avis.

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

UDrake
la source
1
Cette solution est bien meilleure que certaines de celles avec plus de votes positifs, à mon humble avis. Facile à lire, effectue un seul passage dans le tableau et n'alloue ni ne réalloue les tableaux de résultats de partition. J'aime aussi le fait qu'il expose les trois valeurs de filtre communes à la fonction de filtre (valeur, index et tout le tableau). Cela rendra cette fonction beaucoup plus réutilisable.
speckledcarp
J'aime aussi beaucoup celui-ci pour sa simplicité et son élégance. Cela dit, je l'ai trouvé beaucoup plus lent qu'une simple forboucle comme dans une ancienne réponse de @qwertymk. Par exemple, pour un tableau contenant 100 000 éléments, c'était deux fois plus lent sur mon système.
tromgy
9

Dans la fonction de filtrage, vous pouvez pousser vos faux éléments dans une autre fonction extérieure variable:

var bad = [], good = [1,2,3,4,5];
good = good.filter(function (value) { if (value === false) { bad.push(value) } else { return true});

Bien sûr value === false faut être une vraie comparaison;)

Mais il fait presque la même opération comme forEach. Je pense que vous devriez utiliser forEachpour une meilleure lisibilité du code.

nom de code-
la source
D'accord avec l'affiche ci-dessus ... mettre ce genre de logique dans une fonction de filtre se sent un peu gonflé et difficile à gérer.
theUtherSide
Je pense que le filtre a du sens, vous voulez les supprimer du tableau d'origine, donc c'est un filtre
Mojimi
5

Facile à lire.

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)
Matsumoto Kazuya
la source
6
L'inconvénient est que vous faites 2 boucles au lieu d'une seule
Laurent
vote favorable pour la lisibilité, pour les petits tableaux, celui-ci est clairement plus à l'épreuve du temps
allan.simon
4

Essaye ça:

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

qwertymk
la source
Soulignant que la fonction de filtre des tableaux n'est pas réellement prise en charge dans tous les navigateurs (JE VOUS REGARDE PLUS VIEUX IE)
TheZ
4

Et ça?

[1,4,3,5,3,2].reduce( (s, x) => { s[ x > 3 ].push(x); return s;} , {true: [], false:[]} )

C'est probablement plus efficace que l'opérateur de propagation

Ou un peu plus court, mais plus laid

[1,4,3,5,3,2].reduce( (s, x) => s[ x > 3 ].push(x)?s:s , {true: [], false:[]} )

Vereb
la source
2

Beaucoup de réponses ici utilisent Array.prototype.reducepour construire un accumulateur mutable, et soulignent à juste titre que pour les grands tableaux, c'est plus efficace que, par exemple, d'utiliser un opérateur de propagation pour copier un nouveau tableau à chaque itération. L'inconvénient est que ce n'est pas aussi joli qu'une expression "pure" utilisant la courte syntaxe lambda.

Mais un moyen de contourner cela est d'utiliser l'opérateur virgule. Dans les langages de type C, la virgule est un opérateur qui renvoie toujours l'opérande de droite. Vous pouvez l'utiliser pour créer une expression qui appelle une fonction void et renvoie une valeur.

function partition(array, predicate) {
    return array.reduce((acc, item) => predicate(item)
        ? (acc[0].push(item), acc)
        : (acc[1].push(item), acc), [[], []]);
}

Si vous tirez parti du fait qu'une expression booléenne convertit implicitement un nombre comme 0 et 1, et vous pouvez la rendre encore plus concise, même si je ne pense pas qu'elle soit aussi lisible:

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']
parktomatomi
la source
0

Partition ONE-LINER

const partition = (a,f)=>a.reduce((p,q)=>(p[+!f(q)].push(q),p),[[],[]]);

DEMO

// to make it consistent to filter pass index and array as arguments
const partition = (a, f) =>
    a.reduce((p, q, i, ar) => (p[+!f(q, i, ar)].push(q), p), [[], []]);

console.log(partition([1, 2, 3, 4, 5], x => x % 2 === 0));
console.log(partition([..."ABCD"], (x, i) => i % 2 === 0));

Pour Typescript

const partition = <T>(
  a: T[],
  f: (v: T, i?: number, ar?: T[]) => boolean
): [T[], T[]] =>
  a.reduce((p, q, i, ar) => (p[+!f(q, i, ar)].push(q), p), [[], []]);
nkitku
la source
-1

J'ai fini par faire cela parce que c'est facile à comprendre (et entièrement tapé avec dactylographié).

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)
Andreas Gassmann
la source