web-dev-qa-db-fra.com

Filtrer récursivement un tableau d'objets

Frapper un mur avec celui-ci, je pensais le poster ici au cas où une âme bienveillante en rencontrerait un semblable. J'ai des données qui ressemblent à ceci:

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

Je ne sais pas, au moment de l'exécution, quelle sera la profondeur de la hiérarchie, c'est-à-dire combien de niveaux d'objets auront un tableau d'enfants. J'ai quelque peu simplifié l'exemple, il va falloir que je compare les propriétés de la valeur à un tableau de termes de recherche. Supposons pour le moment que je corresponde à value.includes('Hit')

J'ai besoin d'une fonction qui retourne un nouveau tableau, telle que:

  • Aucun objet ne correspondant à aucun enfant, ni à aucune correspondance dans la hiérarchie des enfants, ne doit pas exister dans l'objet de sortie

  • Chaque objet avec un descendant contenant un objet correspondant doit rester

  • Tous les descendants des objets correspondants doivent rester

Je considère qu'un "objet correspondant" est un objet avec une propriété value qui contient la chaîne Hit dans ce cas, et inversement. 

La sortie devrait ressembler à ceci: 

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

Un grand merci à tous ceux qui ont pris le temps de lire jusque-là, publieront ma solution si j'arrive le premier.

26
Nathan Power

Utiliser .filter() et effectuer un appel récursif comme je l’ai décrit dans le commentaire ci-dessus est fondamentalement ce dont vous avez besoin. Il vous suffit de mettre à jour chaque propriété .children avec le résultat de l'appel récursif avant de retourner. 

La valeur de retour est simplement le .length de la collection résultante .children; ainsi, s'il en existe au moins un, l'objet est conservé.

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


Notez que .includes() sur String est ES7, il faudra donc peut-être le corriger pour les navigateurs hérités. Vous pouvez utiliser le .indexOf("Hit") != -1 traditionnel à sa place.


Pour ne pas modifier l'original, créez une fonction de carte qui copie un objet et utilisez-la avant le filtre.

function copy(o) {
  return Object.assign({}, o)
}

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

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

Pour vraiment compresser le code, vous pouvez faire ceci:

var res = input.filter(function f(o) {
  return o.value.includes("Hit") ||
         o.children && (o.children = o.children.filter(f)).length
})

Bien que cela devienne un peu difficile à lire.

28
user1106925

Voici une fonction qui fera ce que vous cherchez. Essentiellement, il testera chaque élément de la variable arr pour rechercher une correspondance, puis appellera récursivement le filtre sur sa children. De plus, Object.assign est utilisé pour que l'objet sous-jacent ne soit pas modifié. 

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

Je pense que ce sera une solution récursive. En voici une que j'ai essayée.

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);
2
Zohaib Ijaz

Sinon, vous pouvez utiliser la méthode _.filterDeep à partir de deepdash extension pour 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;
  }
});

Voici un test complet pour votre cas

0
Yuri Gor