简体   繁体   中英

Using map reduce etc, how would you find the first item matching a certain criteria in a nested array, and stop once found?

How would you find the first item matching a certain criteria in a nested array, and stop once found?

In a 1D array, this is what the Array.find function is for, but how would you do it for a 2D array, and, even neater, for n-dimension array?

Also, I'm trying to come up with a neat solution using es6 and array functions such as find, map, reduce etc, rather than using more traditional loops and variables to maintain state (see one such old-school solution below).

The data may look something like this

const data = [
  {arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
  {arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
]

I'm hoping I can do something similar to array.find (and its predicate / testing function), but I need to go deeper and find eg the first item with val=5. For the data above, I'd expect to get the item with name 'nnn' (not 'ooo'), and have the process end once the first item is found. Similar to Array.find, I want to avoid processing the rest of the data once a matching item is found.

One boring old way to do it would be something like this, with a loop, but that's... boring, and not as neat as the lovely array functions :)

let found
// loop through all data entries in the outer array
for (const d of data) {
  // attempt to find a matching item in the inner array.
  // using array.find means we stop at the first match, yay!
  const theItem = d.arr.find(item => {
    return myPredicate(item)
  })
  // we also need to break out of the loop. ugh!
  if (theItem) {
    found = theItem
    break
  }
}
// return what we found (may be undefined)
return found

Now, I realise that I can do something with find() and some(), say, similar to the answer here ES6 - Finding data in nested arrays , but the problem is that using find on the outer array means that we get back the first item of the outer data array, whereas I want an item from the inner arr array.

const outer = data.find(d => {
  return d.arr.some(item => {
    return myPredicate(item)
  })
})

I would then have to process outer AGAIN to find the item in outer.arr, something like

outer.arr.find(item => myPredicate(item))

This doesn't sit well with me, as the call to some(...) has already gone through and found the matching inner item!

I thought this would be straight forward, and maybe it is, but for one reason or another I got stuck on this little challenge.

I've also looked at the nice traverse library ( https://www.npmjs.com/package/traverse ), but again that seems to be more about traversing through a whole tree rather than stopping and returning once a particular node is found.

Anyone up for a challenge? ;)

The easiest (though slightly ugly) solution would be to assign the matching item to an outer variable when found:

let foundNested;
data.some(subarr => (
  subarr.some((item) => {
    if (myPredicate(item)) {
      foundNested = item;
      return true;
    }
  });
});

You might use .reduce to avoid assigning to an outer variable:

 const myPredicate = ({ val }) => val === 5; const data = [ {arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]}, {arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]} ]; const found = data.reduce((a, { arr }) => ( a || arr.find(myPredicate) ), null); console.log(found); 

Problem is, the reduce won't short-circuit - it'll fully iterate over the outer array regardless. For true short-circuiting, I think I'd prefer using a for..of loop:

 const data = [ {arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]}, {arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]} ]; function findNested(outerArr, myPredicate) { for (const { arr } of outerArr) { for (const item of arr) { if (myPredicate(item)) { return item; } } } } const myPredicate = ({ val }) => val === 5; console.log(findNested(data, myPredicate)); 

You'll want to write your own find function that doesn't take a predicate but a result-producing callback:

function find(iterable, callback) {
    for (const value of iterable) {
        const result = callback(value);
        if (result !== undefined)
            return result;
    }
}

With that, you can write

const data = [
  {arr: [{val:6,name:'aaa'},{val:4,name:'bbb'},{val:8,name:'ccc'}]},
  {arr: [{val:3,name:'mmm'},{val:5,name:'nnn'},{val:9,name:'ppp'},{val:5,name:'ooo'}]}
];
console.log(find(data, ({arr}) => find(arr, o => o.val == 5 ? o : undefined)));

Alternatively, if you want to get all results, flatMap is the perfect tool:

data.flatMap(({arr}) => arr.filter(({val}) => val == 5));

Sure, why not. I'm up for it. This can probably be improved upon. But this will work. Let's say you are trying to find an object with id of 5 in a multidimensional array.

  const arr = [[[{id: 1}], [{id: 2}]], [[{id: 3}]], [[{id: 4}], [{id: 5}], [{id: 6}]]]
  function findObject (obj) {
    if (Array.isArray(obj)) {
      const len = obj.length
      for (let i = 0; i < len; i++) {
        const found = findObject(obj[i])
        if (found) {
          return found
        }
      }
    } else if (obj.id === 5) { // Put your search condition here.
      return obj
    }
  }

  const obj = findObject(arr)
  console.log('obj: ', obj)

This seems to work, but, in my opinion, it's still not clean with that 'found' variable sitting outside the main block and being assigned from inside the nested find block. It's better though. Thoughts?

let found
data.find(d =>
  d.arr.find(item => {
    found = myPredicate(item) ? item : void 0
    return found !== void 0
  }) !== void 0
)
return found

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM