19

I have a javascript structure like below (nested arrays of objects)

var categoryGroups = [
    {
        Id: 1, Categories: [
            { Id: 1 },
            { Id: 2 }, 
        ]

    },
    {
        Id: 2, Categories: [
            { Id: 100 },
            { Id: 200 },
        ]

    }
]

I want to find a child Category object matching an Id, assuming the Category Id's are all unique.

I've got this below, but was wondering if there is a more concise way of doing it:

var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
    categoryGroup = categoryGroups[i];
    for (j = 0; j < categoryGroup.Categories.length; j++) {
        category = categoryGroup.Categories[j];
        if (category.Id === id) {
            found = true;
            break;
        }
    }
    if (found) break;
}
4
  • 1
    I think there is a bug in your code. The condition if (category.Id === id) should actually be if (category.Id === categoryGroup.Id). Correct me if I am wrong Commented Mar 12, 2014 at 6:22
  • I'd recommend using a library like lodash or underscore. More specifically, look at the find method of lodash. Commented Mar 12, 2014 at 6:23
  • Lets say you want to find an item with id 100, do you want to get the output as { Id: 100 }? Commented Mar 12, 2014 at 6:25
  • Does the nesting go further than two levelo? Commented Mar 12, 2014 at 6:25

10 Answers 10

28

Using flatMap in ES2019

const category = categoryGroups.flatMap(cg => cg.Categories).find(c => c.Id === categoryId);
Sign up to request clarification or add additional context in comments.

2 Comments

Awesome! I've been playing around with find, filter, map etc., but I couldn't get the data I needed from within nested obj, tried this and boom! magic!
Perfect!! This works well to retrieve the data from nested obj.
13

Caveat: This uses a couple of Array.prototype functions that were only added in ECMAScript 5 and thus will not work with older browsers unless you polyfill them.

You can loop over all first-level objects in your array, and then filter the categories based on your condition and collect all matches in an array. Your final result will be the first element in the array of matches (no match found if array is empty).

var matches = [];
var needle = 100; // what to look for

arr.forEach(function(e) {
    matches = matches.concat(e.Categories.filter(function(c) {
        return (c.Id === needle);
    }));
});

console.log(matches[0] || "Not found");

JSFiddle: http://jsfiddle.net/b7ktf/1/

References:

Array.prototype.forEach
Array.prototype.concat
Array.prototype.filter

Comments

6

Using only Array.prototype.filter():

If you are sure that the id you are looking for exists, you can do:

var id = 200; // surely it exists
var category = arr.filter(g => g.Categories.filter(c => c.Id === id)[0])[0].Categories.filter(c => c.Id === id)[0];

If you are not sure that it exists:

var id = 201; // maybe it doesn't exist
var categoryGroup = arr.filter(e => e.Categories.filter(c => c.Id === id)[0])[0]; 
var category = categoryGroup ? categoryGroup.Categories.filter(c => c.Id === id)[0] : null;

jsfiddle

Comments

2

Using reduce and recursion :

function nestedSearch(value) {
    return categoryGroups.reduce(function f(acc, val) {
        return (val.Id === value) ? val :
            (val.Categories && val.Categories.length) ? val.Categories.reduce(f, acc) : acc;
    });
}

> try on JSFiddle

Comments

2

If you want to actually return the inner category (instead of just checking for it's presence) you can use reduce:

return categoryGroups.reduce((prev, curr) => {
    //for each group: if we already found the category, we return that. otherwise we try to find it within this group
    return prev || curr.Categories.find(category => category.Id === id);
}, undefined);

This short-circuits on the inner categories, and touches each categoryGroup once. It could be modified to short-cicuit on the categoryGroups as well.

Here's a JS Fiddle demonstration.

1 Comment

Clever! This is the best "native" approach with only one line of indentation. Interesting that undefined is required otherwise initialValue defaults to the first element of the array.
1

check the code in the fiddle

var categoryGroups = [
    {
        Id: 1, Categories: [
            { Id: 1 },
            { Id: 2 }, 
        ]

    },
    {
        Id: 2, Categories: [
            { Id: 100 },
            { Id: 200 },
        ]

    }
]
var id = 100;
var x = 'not found';
var category, categoryGroup, found = false;
for (i = 0; i < categoryGroups.length ; i++) {
    categoryGroup = categoryGroups[i];
    for (j = 0; j < categoryGroup.Categories.length; j++) {
        category = categoryGroup.Categories[j];
        if (category.Id == id) {
            var x = category.Id;
            found = true;
            break;
        }
    }
    if (found) break;
}
alert(x);

The above code checks if id = 100 is found in the array. If found will alert the value else alerts that its not found. value '100' has been hardcoded for the sake of demo

4 Comments

Can you explain why you have set the id = 100?
@Raghu : instead of an input i just hardcoded the value to be checked .. that has to be replaced by the value to b checked as per requirement
got it. would have been great if you could create a function to make it mor e clearer. something like function getCategory(groups, id)
@Raghu : Thanks .. but I just wanted to respond fast to the post.
1

You could wrap it inside a function to get rid of the awkward break; syntax and you can load each element into a variable inside the for(;;) construct to shave off a few lines.

function subCategoryExists(groups, id)
{
  for (var i = 0, group; group = groups[i]; ++i) {
    for (var k = 0, category; category = group.Categories[k]; ++k) {
      if (category.Id == id) {
        return true;
      }
    }
  }
  return false;
}

var found = subCategoryExists(categoryGroups, 100);

3 Comments

This piece of code will not work, if the nesting increases to multiple levels. Thus, it's not scalable.
@SahilBabbar okay, but why did you only leave that comment on this answer?
@AnzilkhaN yeah, you may need a recursive approach if you want to traverse to unknown depths
1

Easy way using lodash library of NodeJS (assuming you are using NodeJS):

const _ = require('lodash');
let category ;
let categoryGroup = _.find(categoryGroups, (element)=>{
  category = _.find(element.Categories, {Id : 100});
  return category;
});

console.log(categoryGroup); // The category group which has the sub category you are looking for
console.log(category); // The exact category you are looking for

Comments

0

You could use underscore:

var cat = _(categoryGroups).
  chain().
  pluck('Categories').
  flatten().
  findWhere({Id: 2}).
  value();

What I'm doing here is that I'm extracting all Categories values in a single array and then grepping for the correct categories.

EDIT: sorry, didn't get your question right the first time. As the comments suggest, you might not want to use underscore just for that, but that's how I would do it :)

5 Comments

I think he wants to find by category id
what if my project doesnt want to use underscore? His question is pretty clear he wants the optimization done in native javascript.
@Raghu, yeah, i wouldn't add underscore for this one task unless you're already using it.
Maybe underscore isn't a good fit for your project, but this is a great option for those who don't mind adding underscore.
No, but it gives enough information that anyone familiar with JS can extend it to peek one level deeper.
0

We are using object-scan for our data processing now. It's very powerful once you wrap your head around it. For your questions this would look like this:

// const objectScan = require('object-scan');

const lookup = (id, data) => objectScan(['Categories.Id'], {
  useArraySelector: false,
  abort: true,
  rtn: 'parent',
  filterFn: ({ value }) => value === id
})(data);

const categoryGroups = [{ Id: 1, Categories: [{ Id: 1 }, { Id: 2 }] }, { Id: 2, Categories: [{ Id: 100 }, { Id: 200 }] }];

console.log(lookup(1, categoryGroups));
// => { Id: 1 }
console.log(lookup(100, categoryGroups));
// => { Id: 100 }
console.log(lookup(999, categoryGroups));
// => undefined
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.