Рекурсивно фильтровать массив объектов

Нажав на стену с этой, подумал, что я отправлю ее здесь, если какая-то душа встретит подобную. У меня есть некоторые данные, которые выглядят примерно так:

const input = [
  {
    value: 'Miss1',
    children: [
      { value: 'Miss2' },
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss5' },
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Miss7',
    children: [
      { value: 'Miss8' },
      { value: 'Miss9', children: [ { value: 'Miss10' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
      { value: 'Miss15', children: [ { value: 'Miss16' } ] }
    ]
  },
];

Во время выполнения я не знаю, насколько глубока иерархия, т.е. сколько уровней объектов будет иметь дочерний массив. Я несколько упростил этот пример, мне действительно нужно будет сопоставить свойства значений с массивом поисковых терминов. Пусть на данный момент предположим, что я сопоставляю, где value.includes('Hit').

Мне нужна функция, которая возвращает новый массив, например:

  • В объекте вывода

    не должен существовать любой объект, не соответствующий совпадению, без дочерних элементов или без совпадений в иерархии детей.
  • Каждый объект с потомком, который содержит соответствующий объект, должен оставаться

  • Все потомки совпадающих объектов должны оставаться

Я рассматриваю "соответствующий объект" как единое целое с свойством value, которое в этом случае содержит строку Hit, и наоборот.

Результат должен выглядеть примерно так:

const expected = [
  {
    value: 'Miss1',
    children: [
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
    ]
  }
];

Большое спасибо всем, кто потратил время на то, чтобы прочитать это, опубликует мое решение, если я получу его первым.

Ответы

Ответ 1

Используя .filter() и делая рекурсивный вызов, как я описал в комментарии выше, в основном то, что вам нужно. Вам просто нужно обновить каждое свойство .children с результатом рекурсивного вызова перед возвратом.

Возвращаемое значение - это только .length получаемой коллекции .children, поэтому, если там хотя бы один, объект сохраняется.

var res = input.filter(function f(o) {
  if (o.value.includes("Hit")) return true

  if (o.children) {
    return (o.children = o.children.filter(f)).length
  }
})

const input = [
  {
    value: 'Miss1',
    children: [
      { value: 'Miss2' },
      { value: 'Hit1', children: [ { value: 'Miss3' } ] }
    ]
  },
  {
    value: 'Miss4',
    children: [
      { value: 'Miss5' },
      { value: 'Miss6', children: [ { value: 'Hit2' } ] }
    ]
  },
  {
    value: 'Miss7',
    children: [
      { value: 'Miss8' },
      { value: 'Miss9', children: [ { value: 'Miss10' } ] }
    ]
  },
  {
    value: 'Hit3',
    children: [
      { value: 'Miss11' },
      { value: 'Miss12', children: [ { value: 'Miss13' } ] }
    ]
  },
  {
    value: 'Miss14',
    children: [
      { value: 'Hit4' },
      { value: 'Miss15', children: [ { value: 'Miss16' } ] }
    ]
  },
];

var res = input.filter(function f(o) {
  if (o.value.includes("Hit")) return true

  if (o.children) {
    return (o.children = o.children.filter(f)).length
  }
})
console.log(JSON.stringify(res, null, 2))

Ответ 2

Здесь функция, которая будет делать то, что вы ищете. По существу, он будет тестировать каждый элемент в arr для соответствия, а затем рекурсивно вызывать фильтр на children. Также используется Object.assign, так что базовый объект не изменяется.

function filter(arr, term) {
    var matches = [];
    if (!Array.isArray(arr)) return matches;

    arr.forEach(function(i) {
        if (i.value.includes(term)) {
            matches.push(i);
        } else {
            let childResults = filter(i.children, term);
            if (childResults.length)
                matches.push(Object.assign({}, i, { children: childResults }));
        }
    })

    return matches;
}

Ответ 3

Я думаю, что это будет рекурсивное решение. Вот что я пробовал.

function find(obj, key) {
  if (obj.value && obj.value.indexOf(key) > -1){
    return true;
  }
  if (obj.children && obj.children.length > 0){
    return obj.children.reduce(function(obj1, obj2){
      return find(obj1, key) || find(obj2, key);
    }, {}); 
  } 
  return false;
}

var output = input.filter(function(obj){
     return find(obj, 'Hit');
 });
console.log('Result', output);

Ответ 4

В качестве альтернативы вы можете использовать _.filterDeep метод из deepdash расширения для lodash:

// We will need 2 passes, first - to collect needed nodes with 'Hit' value:
var foundHit = _.filterDeep(input,
  function(value) {
    if (value.value && value.value.includes('Hit')) return true;
  },{ condense: false, // keep empty slots in array to preserve correct paths
      leafsOnly: false }
);
// second pass - to add missed fields both for found 'Hit' nodes and their parents.
var filtrate = _.filterDeep(input, function(value,key,path,depth,parent,parentKey,parentPath) {
  if (_.get(foundHit, path) !== undefined ||
    _.get(foundHit, parentPath) !== undefined) {
    return true;
  }
});

Вот полный тест для вашего случая