Dividindo uma matriz por função de filtro

92

Eu tenho uma matriz Javascript que gostaria de dividir em duas com base em se uma função chamada em cada elemento retorna trueou false. Essencialmente, este é um array.filter, mas eu gostaria de também ter em mãos os elementos que foram filtrados para fora .

Atualmente, meu plano é usar array.forEache chamar a função de predicado em cada elemento. Dependendo se isso é verdadeiro ou falso, colocarei o elemento atual em um dos dois novos arrays. Existe uma maneira mais elegante ou melhor de fazer isso? Uma array.filterem que o vai empurrar o elemento para uma outra matriz antes de retornar false, por exemplo?

Mike Chen
fonte
Se você puder postar algum código de amostra, ajudará a lhe dar uma resposta melhor!
Mark Pieszak - Trilon.io
Não importa qual implementação você usa, o Javascript sempre terá que: fazer um loop pelos itens, executar a função, colocar o item em um array. Eu não acho que é uma maneira de fazer isso mais eficiente.
TheZ
3
Você pode fazer o que quiser no retorno de chamada transmitido, .filtermas esses efeitos colaterais são difíceis de rastrear e entender. Basta iterar sobre o array e enviar para um array ou outro.
Felix Kling

Respostas:

75

Com ES6, você pode usar a sintaxe de propagação com reduzir:

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 em uma única linha:

const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
braza
fonte
8
Para mim, partição Lodash ou apenas forEach seria mais fácil de entender, mas mesmo assim, bom esforço
Toni Leigh
15
Isso criará dois novos arrays para cada elemento do original. Enquanto um array terá apenas dois elementos, o outro cresce com o tamanho do array. Isso será muito lento e desperdiçará muita memória. (Você poderia fazer o mesmo com um push e seria mais eficiente.)
Stuart Schechter
Obrigado, economizou meu tempo!
7urkm3n
Isso é muito útil, braza. Nota para outros: existem algumas versões mais legíveis abaixo.
Raffi
38

Você pode usar 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
fonte
16

Você pode usar reduzir para isso:

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

Atualizar. Usando a sintaxe ES6, você também pode fazer isso usando recursão:

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
fonte
3
IMHO, a primeira solução (push) é um melhor desempenho conforme o tamanho da matriz aumenta.
ToolmakerSteve
@ToolmakerSteve você poderia explicar mais por quê? Também li o comentário sobre a resposta principal, mas ainda
estou
2
@buncis. A primeira abordagem examina cada elemento uma vez, simplesmente empurrando esse elemento para a matriz apropriada. A segunda abordagem constrói [...left, current]ou [...right, current]- para cada elemento. Não sei exatamente o que é interno, mas tenho certeza de que a construção é mais cara do que simplesmente inserir um elemento em um array. Além disso, como regra geral, a recursão é mais cara do que iterar , porque envolve a criação de um "quadro de pilha" a cada vez.
Toolmaker Steve
14

Isso soa muito semelhante ao método de RubyEnumerable#partition .

Se a função não pode ter efeitos colaterais (ou seja, não pode alterar o array original), então não há maneira mais eficiente de particionar o array do que iterar cada elemento e enviar o elemento para um de seus dois arrays.

Dito isso, é indiscutivelmente mais "elegante" criar um método Arraypara executar essa função. Neste exemplo, a função de filtro é executada no contexto do array original (ou seja, thisserá o array original) e recebe o elemento e o índice do elemento como argumentos (semelhante ao método jQueryeach ):

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
fonte
11
Para o leitor moderno, POR FAVOR, não adicione métodos aos objetos da biblioteca padrão global. É perigoso e provavelmente será sobrescrito, levando a um comportamento misterioso e quebrado. Uma função simples e antiga, com escopo adequado, é muito mais segura e chamar myFunc (array) não é menos "elegante" do que array.myFunc ().
Emmett R.
14

Eu vim com esse carinha. Ele usa para todos e cada um como você descreveu, mas parece limpo e sucinto na minha opinião.

//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
fonte
1
Esta solução é muito melhor do que algumas com mais votos positivos, IMHO. Fácil de ler, faz uma única passagem pelo array e não aloca e realoca os arrays de resultado da partição. Também gosto que exponha todos os três valores de filtro comuns à função de filtro (valor, índice e todo o array). Isso tornará essa função muito mais reutilizável.
speckledcarp
Também gosto muito deste por sua simplicidade e elegância. Dito isso, achei que ele é significativamente mais lento do que um forloop simples, como em uma resposta antiga de @qwertymk. Por exemplo, para um array contendo 100.000 elementos, era duas vezes mais lento no meu sistema.
tromgy
9

Na função de filtro, você pode enviar seus itens falsos para outra variável fora da função:

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

Claro que value === falseprecisa ser uma comparação real;)

Mas ele faz quase a mesma operação como forEach. Eu acho que você deve usar forEachpara melhor legibilidade do código.

nome de código-
fonte
Concordo com o cartaz acima ... colocar este tipo de lógica em uma função de filtro parece um pouco inchado e difícil de gerenciar.
theUtherSide
Acho que o filtro faz sentido, você deseja removê-los do array original para torná-lo um filtro
Mojimi
5

Fácil de ler.

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
fonte
6
A desvantagem é que você faz 2 loops em vez de apenas um
Laurent
voto positivo para legibilidade, para matriz pequena este é claramente mais à prova de futuro
allan.simon
4

Experimente isto:

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
fonte
Indicando que a função de filtro de matrizes não é realmente suportada em todos os navegadores (ESTOU OLHANDO PARA VOCÊS IEs MAIS VELHOS)
TheZ
4

Que tal isso?

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

Provavelmente, isso é mais eficiente que o operador de espalhamento

Ou um pouco mais curto, mas mais feio

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

Vereb
fonte
2

Muitas respostas aqui são usadas Array.prototype.reducepara construir um acumulador mutável e, com razão, apontam que, para grandes arrays, isso é mais eficiente do que, digamos, usar um operador de propagação para copiar um novo array a cada iteração. A desvantagem é que não é tão bonito quanto uma expressão "pura" usando a sintaxe lambda curta.

Mas uma maneira de contornar isso é usar o operador vírgula. Em linguagens como o C, a vírgula é um operador que sempre retorna o operando à direita. Você pode usar isso para criar uma expressão que chama uma função void e retorna um valor.

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

Se você tirar vantagem do fato de que uma expressão booleana é convertida implicitamente em um número como 0 e 1, pode torná-la ainda mais concisa, embora eu não ache que seja tão legível:

function partition(array, predicate) {
    return array.reduce((acc, item) => (acc[+!predicate(item)].push(item), acc), [[], []]);
}

Uso:

const [trues, falses] = partition(['aardvark', 'cat', 'apple'], i => i.startsWith('a'));
console.log(trues); // ['aardvark', 'apple']
console.log(falses); // ['cat']
parktomatomi
fonte
0

Partição 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));

Para texto datilografado

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
fonte
-1

Acabei fazendo isso porque é fácil de entender (e totalmente digitado com texto digitado).

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
fonte