División de una matriz por función de filtro

92

Tengo una matriz de Javascript que me gustaría dividir en dos según si una función llamada en cada elemento devuelve trueo false. En esencia, se trata de una array.filter, pero me gustaría tener también a mano los elementos que se filtraron a cabo .

Actualmente, mi plan es usar array.forEachy llamar a la función de predicado en cada elemento. Dependiendo de si esto es verdadero o falso, insertaré el elemento actual en una de las dos nuevas matrices. ¿Existe una forma más elegante o mejor de hacer esto? ¿ array.filterDónde empujará el elemento a otra matriz antes de que regrese false, por ejemplo?

Mike Chen
fuente
Si puede publicar algún código de muestra, ¡le dará una mejor respuesta!
Mark Pieszak - Trilon.io
No importa qué implementación use, Javascript siempre tendrá que: recorrer elementos, ejecutar la función, empujar el elemento en una matriz. Creo que no hay es una manera de hacer que sea más eficiente.
TheZ
3
Puede hacer lo que quiera en la devolución de llamada transmitida, .filterpero estos efectos secundarios son difíciles de rastrear y comprender. Simplemente itere sobre la matriz y empuje a una matriz u otra.
Felix Kling

Respuestas:

75

Con ES6 puede hacer uso de la sintaxis de propagación con 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);

O en una sola línea:

const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
braza
fuente
8
Para mí, la partición lodash o solo para cada uno sería más fácil de entender, pero aún así, buen esfuerzo
Toni Leigh
15
Esto creará dos nuevas matrices para cada elemento del original. Mientras que una matriz solo tendrá dos elementos, la otra crece con el tamaño de la matriz. Por tanto, esto será muy lento y desperdiciará mucha memoria. (Podría hacer lo mismo con un empujón y sería más eficiente.)
Stuart Schechter
¡Gracias, me salvó el tiempo!
7urkm3n
Esto es realmente útil, braza. Nota para otros: hay algunas versiones más legibles a continuación.
Raffi
38

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

o 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
fuente
16

Puedes usar reducir para ello:

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

Actualizar. Usando la sintaxis de ES6 también puede hacerlo usando la recursividad:

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
fuente
3
En mi humilde opinión, la primera solución (push) es un mejor rendimiento a medida que crece el tamaño de la matriz.
ToolmakerSteve
@ToolmakerSteve, ¿podrías explicar más por qué? También leí el comentario sobre la respuesta principal, pero todavía
estoy
2
@buncis. El primer enfoque examina cada elemento una vez, simplemente empujando ese elemento a la matriz apropiada. El segundo enfoque construye [...left, current]o [...right, current]- para cada elemento. No conozco los componentes internos exactos, pero estoy seguro de que la construcción es más cara que simplemente insertar un elemento en una matriz. Además, como regla general, la recursividad es más cara que la iteración , porque implica la creación de un "marco de pila" cada vez.
ToolmakerSteve
14

Esto suena muy similar al método de RubyEnumerable#partition .

Si la función no puede tener efectos secundarios (es decir, no puede alterar la matriz original), entonces no hay forma más eficiente de particionar la matriz que iterando sobre cada elemento y empujando el elemento a una de sus dos matrices.

Dicho esto, podría decirse que es más "elegante" crear un método Arraypara realizar esta función. En este ejemplo, la función de filtro se ejecuta en el contexto de la matriz original (es decir, thisserá la matriz original), y recibe el elemento y el índice del elemento como argumentos (similar al método de 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
fuente
11
Para el lector moderno, POR FAVOR no agregue métodos a los objetos de la biblioteca estándar global. Es peligroso y es probable que se sobrescriba, lo que lleva a un comportamiento misterioso y quebrantado. Una función antigua simple, con el ámbito adecuado, es mucho más segura, y llamar a myFunc (array) no es menos "elegante" que array.myFunc ().
Emmett R.
14

Se me ocurrió este pequeño. Se usa para todos y cada uno de los que describiste, pero en mi opinión, se ve limpio y conciso.

//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
fuente
1
Esta solución es mucho mejor que algunas de las que tienen más votos a favor, en mi humilde opinión. Fácil de leer, hace una sola pasada a través de la matriz y no asigna ni reasigna las matrices de resultados de la partición. También me gusta que expone los tres valores de filtro comunes a la función de filtro (valor, índice y toda la matriz). Esto hará que esta función sea mucho más reutilizable.
carpa moteada
También me gusta mucho este por su sencillez y elegancia. Habiendo dicho eso, encontré que era significativamente más lento que un forbucle simple como en una respuesta anterior de @qwertymk. Por ejemplo, para una matriz que contiene 100.000 elementos, fue el doble de lento en mi sistema.
tromgy
9

En la función de filtro, puede insertar sus elementos falsos en otra variable fuera de la función:

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

Por supuesto, value === falsedebe ser una comparación real;)

Pero hace casi la misma operación como forEach. Creo que debería usarlo forEachpara una mejor legibilidad del código.

nombre clave-
fuente
De acuerdo con el cartel anterior ... poner este tipo de lógica en una función de filtro se siente un poco hinchado y difícil de manejar.
theUtherSide
Creo que el filtro tiene sentido, desea eliminarlos de la matriz original para que sea un filtro
Mojimi
5

Fácil de leer.

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
fuente
6
La desventaja es que haces 2 bucles en lugar de solo uno
Laurent
voto positivo por legibilidad, para una matriz pequeña, este es claramente más a prueba de futuro
allan.simon
4

Prueba esto:

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

MANIFESTACIÓN

qwertymk
fuente
Señalando que la función de filtro de las matrices no es realmente compatible con todos los navegadores (TE ESTOY MIRANDO A TI MÁS VIEJOS)
TheZ
4

¿Qué pasa con esto?

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

Probablemente esto sea más eficiente que el operador de propagación

O un poco más corto, pero más feo

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

Vereb
fuente
2

Muchas respuestas aquí se usan Array.prototype.reducepara construir un acumulador mutable, y con razón señalan que para arreglos grandes, esto es más eficiente que, digamos, usar un operador de extensión para copiar un nuevo arreglo en cada iteración. La desventaja es que no es tan bonita como una expresión "pura" que utiliza la sintaxis lambda corta.

Pero una forma de evitarlo es usar el operador de coma. En lenguajes similares a C, la coma es un operador que siempre devuelve el operando de la derecha. Puede usar esto para crear una expresión que llame a una función void y devuelva un valor.

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

Si aprovecha el hecho de que una expresión booleana se convierte implícitamente en un número como 0 y 1, y puede hacerlo aún más conciso, aunque no creo que sea tan legible:

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
fuente
0

Partición ONE-LINER

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

MANIFESTACIÓN

// 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 mecanografiado

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

Terminé haciendo esto porque es fácil de entender (y está completamente mecanografiado).

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
fuente