Searching JSON deeply nested Objects with dynamic keys using Lodash? - javascript

I have a nested JSON that I'd like to search through using lodash. How can I get the root object from data if a search term I'm looking for is within certain keys, and with one of the keys being dynamic?
For example, if I have:
"data": [
{
"name": "Bob's concourse"
"activities": [
{
"day": "Monday",
"routines":
{
"Biking":
{
"details": "won 3 trophies"
"type": "road"
},
"Kayaking":
{
"details": "participated in 3 races"
"type": "rhythm"
}
}
}
}
]
},
{..other_data_etc...},
]
activities can be []; it's not guaranteed that it contains any data.
routines keys are dynamic. ie, Biking, Kayaking are dynamic strings. It can be anything.
If I want to search for an races (case insensitive), I want to search specifically in:
data.name
data.activities.routines.* (the dynamic keys)
data.activities.routines.*.details
If any one of those matches, then it will return the root object: { "name": "Bob", ..... }
I was able to get the name to return:
function searchText(collection, searchterm) {
return _.filter(collection, function(o) {
return _.includes(o.name.toLowerCase(), searchterm)
} );
};
But I'm still new to lodash, and I was unable to get any of the nested searches to return correctly, especially with the dynamic keys part.
Could anyone help explain a solution?

Expanding on your existing attempt with lodash:
const obj = {
data: [{
name: 'Bob\'s concourse',
activities: [{
day: 'Monday',
routines: {
Biking: {
details: 'won 3 trophies',
type: 'road'
},
Kayaking: {
details: 'participated in 3 races',
type: 'rhythm'
}
}
}]
}]
};
function search(str, data) {
const searchStr = str.toLowerCase();
// Only return the entries that contain a matched value
return _.filter(data, (datum) => {
// Check if name matches
return _.includes(datum.name, searchStr)
|| _.some(datum.activities, (activity) => {
return _.entries(activity.routines).some(([routine, {details}]) => {
// Check if dynamic routine matches or details
return _.includes(routine, searchStr) || _.includes(details, searchStr);
});
});
});
}
console.log(search('foobar', obj.data));
console.log(search('races', obj.data));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.10/lodash.min.js"></script>
You can also accomplish this with plain JavaScript. Using some newish syntax such as destructuring assignment and newish native methods such as Object.entries essentially follows the same pattern as using lodash:
const obj = {
data: [{
name: 'Bob\'s concourse',
activities: [{
day: 'Monday',
routines: {
Biking: {
details: 'won 3 trophies',
type: 'road'
},
Kayaking: {
details: 'participated in 3 races',
type: 'rhythm'
}
}
}]
}]
};
function search(str, data) {
const regex = RegExp(str, 'i');
// Only return the entries that contain a matched value
return data.filter((datum) => {
// Check if name matches
return regex.test(datum.name)
|| datum.activities.some((activity) => {
return Object.entries(activity.routines).some(([routine, {details}]) => {
// Check if dynamic routine matches or details
return regex.test(routine) || regex.test(details);
});
});
});
}
console.log(search('foobar', obj.data));
console.log(search('races', obj.data));

Related

Grouping elements of array on the basis of property

I have a array as follows:
data = [
{
"id":1
"name":"london"
},
{
"id":2
"name":"paris"
},
{
"id":3
"name":"london"
},
{
"id":4
"name":"paris"
},
{
"id":5
"name":"australia"
},
{
"id":6
"name":"newzearland"
}
]
At runtime this array can have n number of elements. I want to group this array with respect to name attribute. All the elements with same name should be moved to a separate array. I don't know the what value can name have in advance. This is coming at runtime. For example, from above array I want final output as follows:
output:
newArray1 = [
{
"id":1
"name":"london"
},
{
"id":3
"name":"london"
}
]
newArray2 = [
{
"id":2
"name":"paris"
},
{
"id":4
"name":"paris"
}
]
newArray3 = [
{
"id":5
"name":"australia"
}
]
newArray4 = [
{
"id":6
"name":"newzearland"
}
]
How can I do that?
As Teemu has already pointed out in a comment, creating new variables to store the data is not ideal. You would have no way of knowing how many groups you've created and using variables that you can't be sure exist is not the best way to write code. Fortunately, JavaScript has objects, which can store data like this in a much cleaner way. Here's the code I've come up with:
function groupBy(arr, key) {
let res = {}
for (let element of arr) {
if (res.hasOwnProperty(element[key])) {
res[element[key]].push(element)
} else {
res[element[key]] = [element]
}
}
return res
}
This code is not the best, most efficient code ever, but it is written to be easier to understand for someone still learning. This code loops over every element in your data and checks whether our result already contains an array for elements with that name. If there's already an array for elements with that name, the current element is added to it. If there isn't one, a new one is created with the current element inside it. To do exactly what you want, you'd call this function with groupBy(data, "name") and assign it to a new variable like groupedData (THIS DOES NOT MODIFY THE DATA, IT RETURNS A NEW OBJECT OF GROUPED DATA) .
Start by getting all the unique .names, then map them to the original array filtered by each .name:
const data = [{
"id": 1, "name": "london"
},
{
"id": 2, "name": "paris"
},
{
"id": 3, "name": "london"
},
{
"id": 4, "name": "paris"
},
{
"id": 5, "name": "australia"
},
{
"id": 6, "name": "newzearland"
}
];
const newData = [...new Set(data
//Get all names in an array
.map(({name}) => name))]
//For each name filter original array by name
.map(n => data.filter(({name}) => n === name));
console.log( newData );
//OUTPUT: [newArray1, newArray2, .....]
You can get the expected result with grouping by key approach.
const data = [{"id":1,"name":"london"},{"id":2,"name":"paris"},{"id":3,"name":"london"},{"id":4,"name":"paris"},{"id":5,"name":"australia"},{"id":6,"name":"newzearland"}];
const result = Object.values(data.reduce((acc, obj) =>
({ ...acc, [obj.name]: [...(acc[obj.name] ?? []), obj] }), {}));
console.log(result);
const [newArray1, newArray2, newArray3, newArray4, ...rest] = result;
console.log('newArray1:', newArray1);
console.log('newArray2:', newArray2);
console.log('newArray3:', newArray3);
console.log('newArray4:', newArray4);
.as-console-wrapper{min-height: 100%!important; top: 0}

How to access an object nested inside an array by the value of one of its keys? [duplicate]

This question already has answers here:
Filtering object properties based on value
(8 answers)
Closed 3 years ago.
Say I have an object that looks like this:
{
"data": {
"postsConnection": {
"groupBy": {
"author": [
{
"key": "xyz",
"connection": {
"aggregate": {
"count": 5
}
}
},
{
"key": "abc",
"connection": {
"aggregate": {
"count": 3
}
}
}
]
}
}
}
}
How would one access the value of count corresponding to the author element that has, say, xyz as its key? I know for this particular example I could just do this:
const n = data.postsConnection.groupBy.author[0].connection.aggregate.count
But that would mean knowing in advance which element in the array holds the desired value for key, which isn't the case in my context.
If the author can appear multiple times, you can .filter() the array stored at author and then .map() the results to the count:
const data = {data:{postsConnection:{groupBy:{author:[{key:"xyz",connection:{aggregate:{count:5}}},{key:"abc",connection:{aggregate:{count:3}}}]}}}};
const author = "xyz";
const res = data.data.postsConnection.groupBy.author.filter(({key}) => key === author).map(obj => obj.connection.aggregate.count);
console.log(res);
// If you want to get the total of all counts for the given author, you can use reduce on the result to sum:
const totalCountsForAuthor = res.reduce((acc, n) => acc+n, 0);
console.log(totalCountsForAuthor);
If the author can only appear once, you can use .find() instead of .filter() like so:
const data = {data:{postsConnection:{groupBy:{author:[{key:"xyz",connection:{aggregate:{count:5}}},{key:"abc",connection:{aggregate:{count:3}}}]}}}};
const author = "xyz";
const res = data.data.postsConnection.groupBy.author.find(({key}) => key === author).connection.aggregate.count
console.log(res);
You can use Array#find to get the first instance inside an array that meets a certain condition (in your case, the first instance whose key value is equal to the key value you want).
var obj = {"data":{"postsConnection":{"groupBy":{"author":[{"key":"xyz","connection":{"aggregate":{"count":5}}},{"key":"abc","connection":{"aggregate":{"count":3}}}]}}}};
function getAuthorByKey(key) {
return obj.data.postsConnection.groupBy.author.find(author => author.key === key);
}
console.log(getAuthorByKey("xyz").connection.aggregate.count);
console.log(getAuthorByKey("abc").connection.aggregate.count);
If the author array always exists:
const data = {
postsConnection: {
groupBy: {
author: [{
key: "xyz",
connection: {
aggregate: {
count: 5
}
}
}, {
key: "abc",
connection: {
aggregate: {
count: 3
}
}
}]
}
}
};
function getCount(keyVal) {
const element = data.postsConnection.groupBy.author.find(item => item.key === keyVal)
return element.connection.aggregate.count || "";
}
console.log(getCount('xyz'))
var data = { "data": {
"postsConnection": {
"groupBy": {
"author": [
{
"key": "xyz",
"connection": {
"aggregate": {
"count": 5
}
}
},
{
"key": "abc",
"connection": {
"aggregate": {
"count": 3
}
}
}
]
}
}
}
};
data.data.postsConnection.groupBy.author.forEach((autor) => {
if(autor.key === "xyz")
console.log(autor.connection.aggregate);
});
You can make use of array find, to find the author by "key".
Docs: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
const author = data.postsConnection.groupBy.author.find((author) => author.key === "xyz")
CodePen:
https://codepen.io/gmaslic/pen/wvwbLoL
Then you can access all the properties from found "author"

JavaScript: Filtering between the same nested key using lodash

I have an array baseTable, which looks like this:
baseTable = [
{
exid: "2",
name: "aa",
children_meta: {
has: false
}
},
{
exid: "1",
name: "aa1",
children_meta: {
has: false
}
},
{
exid: "3",
name: "bb",
children_meta: {
has: true
},
children: [
{
exid: "101",
name: "c101"
},
{
exid: "102",
name: "c102"
}
]
}
]
and another array againstTable like this:
againstTable = [
{
exid: "2",
name: "aa",
children_meta: {
has: false
}
},
{
exid: "3",
name: "bb",
children_meta: {
has: true
},
children: [
{
exid: "102",
name: "c102"
}
]
}
]
Is there a lodash method to select objects from the baseTable array where the same exid does not exist in the againstTable?
To illustrate, I need method that can produce the following array result from the two arrays above:
[
{
exid: "1",
name: "aa1",
children_meta: {
has: false
}
},
{
exid: "3",
name: "bb",
children_meta: {
has: true
},
children: [
{
exid: "101",
name: "c101"
}
]
}
]
This is how I was trying but this method becomes too big for a small task:
conditionalRender(o: { baseTable; againstTable }) {
const { baseTable, againstTable } = o;
// Check if there are no duplicates in the base
// table; check against, "against table"
// This could be possible after user performs a search
console.log(baseTable, "..base");
console.log(againstTable, "...againsr");
const baseMap = {};
const againstMap = {};
baseTable.forEach(row => (baseMap[row.pid] = row));
againstTable.forEach(row => (againstMap[row.pid] = row));
// const against_ids = new Set(againstTable.map(({ pid }) => pid));
// return baseTable.filter(({ pid }) => !against_ids.has(pid));
const filteredBaseTable: { [index: string]: any } = [];
baseTable.forEach(({ pid }) => {
if (baseMap[pid].children_meta.has) {
// If it is a group, check if there exists
// a part in another table
if (againstMap[pid]) {
// Opposite table also has the same eequipment group
// Will keep the children that are not present in the
// opposite table
// Each child can be differentiated by its exid
const exidsInAgainstTable = new Set(
againstMap[pid].children.map(crow => crow.exid)
);
// Keep only those ids in base table that do not exist in against table
const originalBaseChildren = baseMap[pid].children;
baseMap[pid].children = originalBaseChildren.filter(
({ exid }) => !exidsInAgainstTable.has(exid)
);
filteredBaseTable.push(baseMap[pid]);
}
} else {
if (!againstMap[pid]) {
filteredBaseTable.push(baseMap[pid]);
}
}
});
return filteredBaseTable;
}
This can be achieved without lodash using build-in array reduction.
For instance, you could call reduce on the baseTable array where for each iteration, you search for an item in againstTable that matches on exid.
If no match is found, add the baseItem to your output array (this represents the case where exid: "2" from your data above is added to the result).
If a match is found, examine the children sub arrays of both baseItem and againstItem (if present), and filter items in the baseItem.children array where that child's exid never occours in the againstItem.children sub-array. If the filtered result is non-empty, update the baseItem children array with the filtered result and add that to your output.
One way to express this is code would be:
const baseTable=[{exid:"2",name:"aa",children_meta:{has:false}},{exid:"1",name:"aa1",children_meta:{has:false}},{exid:"3",name:"bb",children_meta:{has:true},children:[{exid:"101",name:"c101"},{exid:"102",name:"c102"}]}];const againstTable=[{exid:"2",name:"aa",children_meta:{has:false}},{exid:"3",name:"bb",children_meta:{has:true},children:[{exid:"102",name:"c102"}]}];
const result = baseTable.reduce((output, baseItem) => {
const matchOnExid = againstTable.find(againstItem => {
return againstItem.exid === baseItem.exid;
});
if (matchOnExid) {
/* If match of exid found from agaistTable for current baseTable item
then examine the children sub-arrays */
const baseChildren = baseItem.children;
const againstChildren = matchOnExid.children;
if (Array.isArray(baseChildren) && Array.isArray(againstChildren)) {
/* If valid children sub-arrays exist of items, filter a subset of the
baseItem children for items that do not exist in the children sub-array
of the matched againstItem */
const matchChildrenOnExid = baseChildren.filter(baseChildItem =>
{
return againstChildren.every(againstChildItem => {
return againstChildItem.exid !== baseChildItem.exid;
});
});
if (matchChildrenOnExid.length > 0) {
/* If a subset of children do exist, then baseItem can be added to
resulting array. Note also that we need to update the children array
of the returned result to reflect the subset that was just found */
output.push({ ...baseItem,
children: matchChildrenOnExid
});
}
}
} else {
/* If no match of exid found, just add the baseItem to the
result */
output.push(baseItem);
}
return output;
}, []);
console.log(result);

Return all property names that share the same value in JS Object

I have an array of objects and I want to return array containing only the names of the happy people and return all names when everybody is happy.
The thing I fail to get is to get all names when everybody is happy. Any ideas?
EDIT: This is the object.
[
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
function findHappyPeople(people) {
var happyPeople = Object.keys(people).filter(function(key) {
if(people[key] === 'Happy') {
return people[name]
}
});
return happyPeople;
}
You have an array of objects, so Object.keys() wouldn't be needed here.
You can use a .map() operation after the filter to end up with an array of names.
Your people[name] code isn't going to work because you have no name variable, except the global one if you're in a browser, which isn't what you want. Your data has a .name property, so use that.
const data = [
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
console.log(findHappyPeople(data));
function findHappyPeople(people) {
return people
.filter(function(p) { return p.disposition === "Happy" })
.map(function(p) { return p.name });
}
Or with arrow function syntax:
const data = [
{ name: 'Don', disposition: 'Happy' },
{ name: 'Trev', disposition: 'Happy' },
]
console.log(findHappyPeople(data));
function findHappyPeople(people) {
return people
.filter(p => p.disposition === "Happy")
.map(p => p.name);
}

Is there a simple way to map nested data with Lodash?

For my current project, I'm working with an API that returns data formatted like this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", ... },
{ name: "name2", ... },
{ name: "name3", ... },
...
],
...
},
...
]
},
...
],
...
};
I'd like to create a pure function, mapValues, that takes in an object in the above format, as well as an object mapping each name to a value, and returns the same structure, but with each point containing the value that corresponds to its name.
For example, calling mapValues(data, { name1: "value1", name2: "value2", name3: "value3" }) should return this:
{
groups: [
{
items: [
{
points: [
{ name: "name1", value: "value1", ... },
{ name: "name2", value: "value2", ... },
{ name: "name3", value: "value3", ... },
...
],
...
},
...
]
},
...
],
...
};
Here's my first pass:
function mapValues(data, values) {
return _.extend({}, data, {
groups: _.map(ui.groups, (group) => {
return _.extend({}, group, {
items: _.map(group.items, (item) => {
return _.extend({}, item, {
points: _.map(item.points, (point) => {
return _.extend({ value: values[point.name] }, point);
})
});
})
});
})
});
}
That works, but there's quite a bit of nesting a duplicate code. For my second attempt, I reached for recursion.
function mapValues(data, values) {
return (function recursiveMap(object, attributes) {
if (attributes.length === 0) { return _.extend({ value: values[object.name] }, object); }
let key = attributes[0];
return _.extend({}, object, {
[key]: _.map(object[key], child => recursiveMap(child, attributes.slice(1)))
});
})(ui, ["groups", "items", "points"]);
}
That works too, but it's difficult to read and not very concise.
Is there a cleaner way to recursively map an object using Lodash? Ideally, I'm looking for a functional approach.
Here's a way you can do it using Object.assign and no fancy functions
var data = <your data here>;
var values = <your mapped values>;
data.groups.items.points.map(p=>
Object.assign(p, {value: values[p.name]})
);
This works because arrays and objects are pass by reference. So any modifications to the values will result in the original being changed.
If you don't want to mutate your original dataset, it requires you to use {} as the first argument (to assign to a new, empty object) and show clear read/write paths for each object.
var newData = Object.assign({}, data,
{groups:
{items:
{points: data.groups.items.points.map(p=>
Object.assign({}, p, {value: values[p.name]})
)}
}
}
);
I know you wanted Lodash, but I had same issue some time ago and I've came up with this JSFiddle I am using great tool created by nervgh. It is very simple yet usefull. You can adjust this function to be pure using Object.assign, accept key parameter and tweak it however you want. You get the idea.
function mapValues(data, values) {
var iterator = new RecursiveIterator(data);
for(let {node} of iterator) {
if(node.hasOwnProperty('name')) {
node.value = values[node.name];
}
}
return data;
}

Categories