Рекурсивно фильтровать массив объектов
Нажав на стену с этой, подумал, что я отправлю ее здесь, если какая-то душа встретит подобную. У меня есть некоторые данные, которые выглядят примерно так:
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;
}
});
Вот полный тест для вашего случая