11

I have an object of folders/files that looks like this:

{
  about.html : {
    path : './about.html'
  },
  about2.html : {
    path : './about2.html'
  },
  about3.html : {
    path : './about3.html'
  },
  folderName : {
    path : './folderName',
    children : {
      sub-child.html : {
        path : 'folderName/sub-child.html'
      }
    }
  }
}

And it can go 6-7 levels deep of folders having children.

I want to find the object where path is equal to a string that I provide. Regardless of how deep it is.

I'm using underscore which only does top level:

_.findWhere(files,{path:'./about2.html'}

How can I do a deep, nested search. Does underscore have something for this or do I need to build a mixin with recursion?

wesbos
  • 25,839
  • 30
  • 106
  • 143

4 Answers4

12

This isn't the prettiest code, but I tested it out and it seems to work the way you are asking. It's setup as a lodash/underscore mixin, but can be used however. Usage would be like this:

_.findDeep(testItem, { 'path': 'folderName/sub-child.html' })

Implementation:

findDeep: function(items, attrs) {

  function match(value) {
    for (var key in attrs) {
      if(!_.isUndefined(value)) {
        if (attrs[key] !== value[key]) {
          return false;
        }
      }
    }

    return true;
  }

  function traverse(value) {
    var result;

    _.forEach(value, function (val) {
      if (match(val)) {
        result = val;
        return false;
      }

      if (_.isObject(val) || _.isArray(val)) {
        result = traverse(val);
      }

      if (result) {
        return false;
      }
    });

    return result;
  }

  return traverse(items);

}
dbau
  • 16,009
  • 2
  • 21
  • 31
dariusriggins
  • 1,434
  • 1
  • 15
  • 30
  • The return false is there for lodash to break out of the loop if a value is found, not sure if underscore supports that, looking through the code, perhaps returning {} would cause it to break out, but I'm not sure. – dariusriggins Jul 10 '13 at 20:26
  • Great solution, works like a charm. I wonder why this isn't built into US/LD by default! – dbau Jun 21 '14 at 07:55
  • @terox could you possibly elaborate? i do not believe that, as cycles are possible in objects too - so, nust following the route... –  Jan 19 '16 at 23:25
9

Instead of findWhere, use filter, which takes a function as the predicate rather than a key-value map. Use a recursive function to check the current node and possible children. Something like this:

var searchText = './about2.html';

var recursiveFilter = function(x) {
    return x.path == searchText || 
        ( typeof x.children != 'undefined' && recursiveFilter(x.children['sub-child.html']) );
};

_.filter(files, recursiveFilter);

Edit

Assuming this works, you'll probably want to make a function getRecursiveFilter(searchText). Here's how that would look:

function getRecursiveFilter(searchText) { 
    var recursiveFilter = function(x) {
        return x.path == searchText || 
            (typeof x.children != 'undefined' 
                && arguments.callee(x.children['sub-child.html']) );
    };
    return  recursiveFilter;
}

Note that here, recursiveFilter uses arguments.callee to call itself recursively.


Here's a working demo.

Community
  • 1
  • 1
McGarnagle
  • 101,349
  • 31
  • 229
  • 260
  • Is it possible to have it without the 'subl-child.html' hardcoded? – wesbos Jul 10 '13 at 19:44
  • @Wes sure, just add it as another parameter next to `searchText`, I guess? See here: http://jsfiddle.net/Fy9Ej/1/ – McGarnagle Jul 10 '13 at 19:46
  • Sorry, I want to find the object where path === string. Regardless of its top level or 100 levels deep. There aren't two strings here. – wesbos Jul 10 '13 at 20:04
6

This already has an accepted answer, but this other answer was very clean and perfect for my similar situation: https://stackoverflow.com/a/21600748/1913975 _.filter +_.where

Community
  • 1
  • 1
abenrob
  • 878
  • 10
  • 23
1

Though accepted answer works, it's too generic - it searches all the properties of an object to find children. I am proposing introducing an extra parameter, called 'recursProperty' which will be considered to go deep in the object. This solution is also setup to be used as lodash/underscore mixin and extends loadash/underscore capabilities.

_.findDeep = function(collection, predicate, recursProperty){
    let items = [];
    _.each(collection, each => items.push(each));
    return _.find(items, function(value, key, coll){
        if (predicate(value, key, coll)){
            return true;
        } else {
            _.each(value[recursProperty], each => items.push(each));
        }
    });
};

It can be used as any other underscore function. e.g,

_.findDeep(self.baseEntities, baseEntity => baseEntity.id === 71, 'entity');

Not providing proper value for 'recursProperty' argument or providing null/undefined will simply make the search only on first level (no going deep).

adhiroha
  • 71
  • 5