0

Given a JavaScript object which represents a JSON like so -

[
    {
        "Id": "8868dfdd-9b4e-4bad-a4ce-ecae6a3cc828",
        "Name": "Company 1",
        "Locations": [
            {
                "Id": "bd017b9c-b62e-43aa-9f00-c164a855eed1",
                "Name": "Location 1",
                "Departments": [
                    {
                        "Id": "c9e4afe3-bbdb-474f-9062-2935025bfa2e",
                        "Name": "Department 1",
                        "Employees": [
                            {
                                "Id": "92c3a085-5712-422d-8b0f-922b57889c4f",
                                "Name": "Employee 1",
                                "Title": "FrontEnd Engineer",
                                "Location": "New York",
                                "Photo": ""
                            }
                        ]
                    }
                ]
            }
        ]
    }
]

I want to filter this data structure by employee name, given that there might be multiple company, location, department. Here is my attempt at it but clearly it is not working due to my understanding of how Array.filter or Array.reduce works.

filterContacts = search => {
    if (search.trim() === "") {
        this.setState({ filteredContacts: null, search: search });
    } else {
        let filteredArray = this.state.contacts.reduce((f, c) => {
            let clone = [];
            for (let i = 0; i < c.Locations.length; i++) {
                const l = c.Locations[i];
                for (let j = 0; j < l.Departments.length; j++) {
                    const d = l.Departments[j];
                    for (let k = 0; k < d.Employees.length; k++) {
                        const e = d.Employees[k];
                        if (e.Name.search(new RegExp(search, "i") > -1)) {
                            clone.push(l);
                        }
                    }
                }
            }
            return clone;
        }, []);
        this.setState({ filteredContacts: filteredArray, search: search });
    }
};

Any help would be much appreciated. Thanks.

2
  • What sort of output are you expecting? Commented Dec 25, 2018 at 17:25
  • If you change let clone = []; to let clone = f; it should work. But can be simplified using .some Commented Dec 25, 2018 at 17:30

2 Answers 2

2

When you use:

 let clone = [];

at the top of the reduce() callback, you throw away the accumulator — the array that keeps getting passed in the loop which is being passed as f in your code. You should use the same reduce accumulator each time and push into it. At the end you'll have an array of all the values:

let arr = [{"Id": "8868dfdd-9b4e-4bad-a4ce-ecae6a3cc828","Name": "Company 1","Locations": [{"Id": "bd017b9c-b62e-43aa-9f00-c164a855eed1","Name": "Location 1","Departments": [{"Id": "c9e4afe3-bbdb-474f-9062-2935025bfa2e","Name": "Department 1","Employees": [{"Id": "92c3a085-5712-422d-8b0f-922b57889c4f","Name": "Employee 1","Title": "FrontEnd Engineer","Location": "New York","Photo": ""}]}]}]}]

let emp = arr.reduce((f, obj) => {
  obj.Locations.forEach(location => 
    location.Departments.forEach(department => 
      f.push(...department.Employees.filter(emp => emp.Name == "Employee 1"))
    )
  )
  return f
}, []) // <-- this array will get passed to every loop as `f`

console.log(emp)

EDIT based on comment

If you want to persevere the structure you can filter the arrays based on the length of the filtered array below them. Here's an example with some extra data see the filtering work, The first one is completely filtered the third has two employees with the same name. Basically it will preserve any item the has location that has a department that has a matching employee:

let arr = [
  {"Id": "someother","Name": "Company 2","Locations": [{"Id": "loc2Id","Name": "Location 2","Departments": [{"Id": "d2","Name": "Department 2","Employees": [{"Id": "emp","Name": "Employee 2","Title": "FrontEnd Engineer","Location": "New York","Photo": ""}]}]}]}, 
  {"Id": "8868dfdd-9b4e-4bad-a4ce-ecae6a3cc828","Name": "Company 1","Locations": [{"Id": "bd017b9c-b62e-43aa-9f00-c164a855eed1","Name": "Location 1","Departments": [{"Id": "c9e4afe3-bbdb-474f-9062-2935025bfa2e","Name": "Department 1","Employees": [{"Id": "92c3a085-5712-422d-8b0f-922b57889c4f","Name": "Employee 1","Title": "FrontEnd Engineer","Location": "New York","Photo": ""}]}]}]},
  {"Id": "someother","Name": "Company 2","Locations": [{"Id": "loc2Id","Name": "Location 2","Departments": [{"Id": "d2","Name": "Department 2","Employees": [{"Id": "emp","Name": "Employee 1","Title": "FrontEnd Engineer","Location": "New York","Photo": ""}, {"Id": "emp","Name": "Employee 1","Title": "FrontEnd Engineer 2","Location": "New York","Photo": ""}]}]}]}, 
]


let f = []
let emp = arr.filter(arr => 
  arr.Locations.filter(location => 
    location.Departments.filter(department => {
      let emp = department.Employees.filter(emp => emp.Name == "Employee 1")
      return emp.length ? emp: false 
    }
    ).length
  ).length
) // <-- this array will get passed to every loop as `f`

console.log(emp)

Sign up to request clarification or add additional context in comments.

3 Comments

Is usage of forEach is a personal preference? I upvoted, especially because I saw .push(...items) - never thought this syntax possible, good to know. I'd use .map though, but this is my personal preference.
Thanks @ValeryBaranov. Yes, a regular for loop would also work (and is sometimes faster). I think forEach easier on the eyes. It's less wordy and easier to see exactly what's going on (if you use good variable names). map() isn't really appropriate here because you're not returning arrays from those loops so there's no reason to insure the overhead of making them.
@MarkMeyer The output I want is to preserve the Company -> Location -> Department hierarchy, so that when I'm rendering again the items are filtered right from the top level node.
1

Here is another short version using map:

var rx=new RegExp(search,'i'),emp=[];
obj.map(c=>
 c.Locations.map(l=>
 l.Departments.map(d=>
 d.Employees.map(e=>
  {if(e.Name.match(rx)) emp.push(e)}
))));

search contains the case-insensitive search pattern. The result is emp, an array of employee objects.

As mentioned above, map is not really necessary and could be replaced by forEach, but in my opinion it is easier to write and does not not really cause significantly more overhead.

Edit, this time using reduce():

It’s Christmas and with too much time on my hands I’ve been playing around further. The following solution will filter out the sought employees without showing their non-matching colleagues and leaving the original array intact:

const rd=(prop,fun)=>
             (a,el)=>{
var arr=el[prop].reduce(fun,[]);
if(arr.length){
  var r=Object.assign({},el);
// alternatively: use spread operator
// var r={...el};
  r[prop]=arr;a.push(r);}
return a;}

var rx=new RegExp('employee 1','i');

 var f=ma.reduce(
  rd('Locations',
  rd('Departments',
  rd('Employees',(a,e)=>{
     if(e.Name.match(rx))
      a.push(e);
     return a;}
,[]),[]),[]),[]);

f will contain an array containing only those locations, departments and employees where the employees will match the regular expression rx.

rd() is a generator function returning the actual filter functions that are being used at three different reduce-levels.

The static Object.assign() function is an easy way of generating a shallow object copy (similar to the slice() method for arrays).

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.