Разделение массива функцией фильтрации

92

У меня есть массив Javascript, который я хотел бы разделить на два в зависимости от того, возвращает ли функция, вызываемая для каждого элемента, trueили false. По сути, это array.filter, но я хотел бы также иметь под рукой те элементы , которые были отфильтрованы из .

В настоящее время я планирую использовать array.forEachи вызывать функцию предиката для каждого элемента. В зависимости от того, истинно это или ложно, я помещаю текущий элемент в один из двух новых массивов. Есть ли более элегантный или лучший способ сделать это? array.filterГде будет толкать элемент на другой массив , прежде чем он возвращается false, например?

Майк Чен
источник
Если вы можете опубликовать образец кода, это поможет вам лучше ответить!
Марк Пешак - Trilon.io
Независимо от того, какую реализацию вы используете, Javascript всегда должен: перебирать элементы, запускать функцию, помещать элемент в массив. Я не думаю, что есть способ сделать это более эффективным.
TheZ
3
Вы можете делать все, что хотите, в переданном обратном вызове, .filterно такие побочные эффекты трудно отследить и понять. Просто перебирайте массив и переходите к одному или другому массиву.
Felix Kling

Ответы:

75

С ES6 вы можете использовать синтаксис распространения с помощью 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);

Или в одной строке:

const [pass, fail] = a.reduce(([p, f], e) => (e > 5 ? [[...p, e], f] : [p, [...f, e]]), [[], []]);
Braza
источник
8
Для меня было бы легче понять раздел lodash или просто forEach, но даже в этом случае хорошее усилие
Тони Ли
15
Это создаст два новых массива для каждого элемента в оригинале. В то время как один массив будет содержать только два элемента, другой будет расти вместе с размером массива. Таким образом, это будет очень медленно и тратить много памяти. (Вы можете сделать то же самое с толчком, и это будет более эффективно.)
Стюарт Шехтер,
Спасибо, сэкономили время!
7urkm3n
Это действительно полезно, braza. Примечание для других: ниже есть несколько более читаемых версий.
Раффи,
38

Вы можете использовать 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']]

или 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' }  ]
Золтан Кочан
источник
16

Для этого можно использовать сокращение:

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

Обновить. Используя синтаксис ES6, вы также можете сделать это с помощью рекурсии:

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]]);
}
Яременко Андрей
источник
3
IMHO, первое решение (push) - лучшая производительность по мере роста размера массива.
ToolmakerSteve
@ToolmakerSteve, не могли бы вы подробнее объяснить, почему? Я также прочитал комментарий к
главному
2
@buncis. Первый подход проверяет каждый элемент один раз, просто помещая этот элемент в соответствующий массив. Второй подход строит [...left, current]или [...right, current]- для каждого элемента. Я не знаю точного внутреннего устройства, но уверен, что построение обходится дороже, чем просто вставка элемента в массив. Кроме того, как правило, рекурсия дороже, чем итерация , поскольку она включает в себя каждый раз создание «кадра стека».
ToolmakerSteve,
14

Это очень похоже на метод РубиEnumerable#partition .

Если функция не может иметь побочных эффектов (т.е. она не может изменить исходный массив), тогда нет более эффективного способа разбить массив, чем итерация по каждому элементу и перемещение элемента в один из двух массивов.

При этом, возможно, более «элегантно» создать метод Arrayдля выполнения этой функции. В этом примере функция фильтра выполняется в контексте исходного массива (т.е. thisбудет исходным массивом), и она получает элемент и индекс элемента в качестве аргументов (аналогично методу 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
источник
11
Для современного читателя, ПОЖАЛУЙСТА, не добавляйте методы к объектам глобальной стандартной библиотеки. Это опасно и может быть перезаписано, что приведет к загадочному и неправильному поведению. Обычная старая функция с правильной областью видимости намного безопаснее, а вызов myFunc (array) не менее «элегантен», чем array.myFunc ().
Emmett R.
14

Я придумал этого маленького парня. Он использует для всех без исключения то, что вы описали, но, на мой взгляд, он выглядит чистым и лаконичным.

//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
источник
1
Это решение намного лучше, чем некоторые из тех, у которых больше голосов, ИМХО. Легко читается, выполняет один проход по массиву и не выделяет и не перераспределяет массивы результатов разделов. Мне также нравится, что он предоставляет функции фильтрации все три общих значения фильтра (значение, индекс и весь массив). Это сделает эту функцию более многоразовой.
speckledcarp
Еще мне этот очень нравится своей простотой и элегантностью. Сказав это, я обнаружил, что это значительно медленнее, чем простой forцикл, как в старом ответе @qwertymk. Например, для массива, содержащего 100 000 элементов, в моей системе он был в два раза медленнее.
tromgy
9

В функции фильтра вы можете поместить свои ложные элементы в другую переменную вне функции:

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

Конечно value === falseнужно реальное сравнение;)

Но он делает почти такую ​​же операцию, как forEach. Я думаю, вам стоит использовать forEachдля лучшей читаемости кода.

кодовое имя-
источник
Согласен с вышеупомянутым плакатом ... включение такой логики в функцию фильтра кажется немного раздутым и трудным в управлении.
theUtherSide 03
Я думаю, что фильтр имеет смысл, вы хотите удалить их из исходного массива, чтобы он стал фильтром
Mojimi
5

Легко читать.

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)
Мацумото Кадзуя
источник
6
Лоран
Проголосуйте за удобочитаемость, для небольшого массива это ясное доказательство будущего
allan.simon
4

Попробуй это:

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

ДЕМО

qwertymk
источник
Указывая на то, что функция фильтрации массивов фактически не поддерживается во всех браузерах (Я СМОТРИТЕ НА ВАС СТАРЫЕ IE)
TheZ
4

Как насчет этого?

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

Возможно, это более эффективно, чем оператор спреда

Или чуть короче, но уродливее

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

Вереб
источник
2

Многие ответы здесь используют Array.prototype.reduceдля создания изменяемого аккумулятора и справедливо указывают, что для больших массивов это более эффективно, чем, скажем, использование оператора распространения для копирования нового массива на каждой итерации. Обратной стороной является то, что это не так красиво, как «чистое» выражение с использованием короткого лямбда-синтаксиса.

Но способ обойти это - использовать оператор запятой. В языках типа C запятая - это оператор, который всегда возвращает правый операнд. Вы можете использовать это для создания выражения, которое вызывает функцию void и возвращает значение.

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

Если вы воспользуетесь преимуществом того факта, что логическое выражение неявно преобразуется в число как 0 и 1, и вы можете сделать его еще более кратким, хотя я не думаю, что это так удобно для чтения:

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

Применение:

const [trues, falses] = partition(['aardvark', 'cat', 'apple'], i => i.startsWith('a'));
console.log(trues); // ['aardvark', 'apple']
console.log(falses); // ['cat']
парктоматоми
источник
0

ОДНОСЛОЙНАЯ перегородка

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

ДЕМО

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

Для машинописного текста

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), [[], []]);
нкитку
источник
-1

В итоге я сделал это, потому что это легко понять (и полностью набрано машинописным текстом).

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)
Андреас Гассманн
источник