I currently have an array of objects with each containing an array of objects.
My goal is to find a nested object in a single line of javascript code. I am trying to avoid a nested loop, I know how to solve it that way but javascript offers a lot of elegant solutions for such scenarios.
This is how my data looks like:
const items = [
{
id: 1,
title: 'Test 1',
data: [
{
id: 3,
title: 'Test 3',
},
{
id: 4,
title: 'Test 4',
},
],
},
{
id: 2,
title: 'Test 2',
data: [
{
id: 5,
title: 'Test 5',
},
],
},
];
With a nested loop:
let item = null;
for (let i = 0; i<items.length; i++) {
for (let j = 0; j<items[i].data; j++) {
if (items[i].data[j].id == myId) {
item = items[i].data[j];
return;
}
}
}
I feel like there is a more simplistic and prettier solution than this.
You could do that in one line. As I saw in your nested loop solution, you find in each object's data, so first you could flatten the data in to array of objects and then find through that array
const items = [
{
id: 1,
title: "Test 1",
data: [
{
id: 3,
title: "Test 3",
},
{
id: 4,
title: "Test 4",
},
],
},
{
id: 2,
title: "Test 2",
data: [
{
id: 5,
title: "Test 5",
},
],
},
];
const myId = 4;
const res = items.flatMap((item) => item.data).find(({ id }) => id === myId);
console.log(res);
In terms of performance, I would suggest the way is to use Array#reduce combined with Array#find
const items = [{id:1,title:"Test 1",data:[{id:3,title:"Test 3",},{id:4,title:"Test 4",},],},{id:2,title:"Test 2",data:[{id:5,title:"Test 5",},],},];
const myId = 4;
const res = items.reduce((acc, {data}) => {
const foundItem = data.find(({id}) => id === myId);
if(foundItem)
acc.push(foundItem);
return acc;
}, [])
console.log(res);
Related
I am attempting to build a new object from an existing deep nested object. I can't seem to get my mind in recurive mode but I am running into a bit of trouble:
oldObjArr = [{
id:1,
name:"Record1"
},{
id:2,
name:"Record2"
},{
id:3,
name:"Record3",
kids:[{
id: 4,
name: "Child 3-1"
},{
id: 5,
name: "Child 3-2"
}]
}]
buildTreeNodes = (node) => {
let data = []
node.map(record=>{
record["icon"] = "..."
record["color"] = "..."
data.push(record)
record.kids && buildTreeNodes(record.kids)
})
}
let newObjArr = buildTreeNodes(oldObjArr)
This OBVIOUSLY does not work, but I can't figure out what will. The resulting object should look like this:
[{
id:1,
name:"Record1",
icon:"...",
color: "...",
},{
id:2,
name:"Record2",
icon:"...",
color: "...",
},{
id:3,
name:"Record3",
icon:"...",
color: "...",
kids:[{
id: 4,
name: "Child 3-1",
icon:"...",
color: "...",
},{
id: 5,
name: "Child 3-2",
icon:"...",
color: "...",
}]
}]
Thanks for any help.
Robert's answer is correct.
If by chance you also want to not mutate the original object, then you can do something like this.
Also using ES6 features coz why not.
const oldObjArr = [{
id: 1,
name: "Record1"
}, {
id: 2,
name: "Record2"
}, {
id: 3,
name: "Record3",
kids: [{
id: 4,
name: "Child 3-1"
}, {
id: 5,
name: "Child 3-2"
}]
}];
function transformObject(item) {
if (Array.isArray(item.kids))
return {
...item, icon: '...', color: '...',
kids: item.kids.map(transformObject)
};
else
return {...item, icon: '...', color: '...' };
}
const newArray = oldObjArr.map(transformObject);
console.log(newArray);
So you iterate over you array and take each object and then add your props to it. Then you check if kids exist and some check if is array. i use instanceof but like #Heretic Monkey point it can be Array.isArray. What more you can setup type guard on front of function check that array argument is array then this you don't have to check that if kids is type of array.
const oldObjArr = [{
id:1,
name:"Record1"
},{
id:2,
name:"Record2"
},{
id:3,
name:"Record3",
kids:[{
id: 4,
name: "Child 3-1"
},{
id: 5,
name: "Child 3-2"
}]
}]
const addKeys = arr => {
for(const obj of arr){
obj['icon'] = "test"
obj['color'] = "test"
if("kids" in obj && obj.kids instanceof Array){
addKeys(obj.kids);
}
}
}
addKeys(oldObjArr)
console.log(oldObjArr)
V2
const addKeys = arr => {
if(!Array.isArray(arr))
return;
for(const obj of arr){
if(typeof obj !== "object")
continue;
obj['icon'] = "test"
obj['color'] = "test"
if("kids" in obj){
addKeys(obj.kids);
}
}
}
Ok check this out:
buildTreeNodes = (node) => {
let data = node.map(record=>{
record["icon"] = "..."
record["color"] = "..."
if (record.kids) record.kids = buildTreeNodes(record.kids);
return record;
})
return data;
}
let newObjArr = buildTreeNodes(oldObjArr)
console.log(newObjArr)
I think this is what you were after. You have to return record with each iteration of map, and it will add it directly to data array. The recursion within works the same.
All details are commented in demo below
let objArr = [{
id: 1,
name: "Record 1"
}, {
id: 2,
name: "Record 2"
}, {
id: 3,
name: "Record 3",
kids: [{
id: 4,
name: "Child 3-1"
}, {
id: 5,
name: "Child 3-2"
}]
},
/*
An object with a nested object not in an array
*/
{
id: 6,
name: 'Record 6',
kid: {
id: 7,
name: 'Child 6-1'
}
},
/*
An object that's filtered out because it doesn't have 'id' key/property
*/
{
no: 0,
name: null
},
/*
An object that's filtered out because it doesn't have 'id' key/property BUT has a nested object that has 'id'
*/
{
no: 99,
name: 'Member 99',
kid: {
id: 8,
name: 'Scion 99-1'
}
}
];
/*
Pass an object that has the key/value pairs that you want added to other objects
*/
const props = {
icon: '...',
color: '...'
};
/*
Pass...
a single object: {obj} of objArr[]
a single key/property: 'id'
an object that contains the key/value pairs to be added to each object that has key/property of id: {props}
*/
const addProp = (obj, prop, keyVal) => {
/*
Convert {props} object into a 2D array
props = {icon: '...', color: '...'}
~TO~
kvArr = [['icon', '...'], ['color', '...']]
*/
let kvArr = Object.entries(keyVal);
/*
for Each key/value pair of kvArr[][]
assign them to the (obj} if it has ['prop']
as one of it's key/properties
(in this demo it's 'id')
*/
kvArr.forEach(([key, val]) => {
if (obj[prop]) {
obj[key] = val;
}
});
/*
Convert {obj} into a 2D array
obj = {id: 3, name: "Record 3", kids: [{ id: 4, name: "Child 3-1"}, {id: 5, name: "Child 3-2"}]}
~TO~
subArr = [['id', 3], ['name', "Record 3"], ['kids', [{id: 4, name: "Child 3-1"}, {id: 5, name: "Child 3-2"}]]
*/
let subArr = Object.entries(obj);
/*
for Each value of subArr[][] (ie ['v'])
if it's an [Array] call addProp and pass
the {obj} of subArr[][]
*/
/*
if it's an {obj} do the same as above
*/
subArr.forEach(([k, v]) => {
if (Array.isArray(v)) {
v.forEach(subObj => {
addProp(subObj, prop, keyVal);
});
} else if (v instanceof Object) {
addProp(v, prop, keyVal);
}
});
};
// Run addProp() on each {obj} of objArr[]
for (let object of objArr) {
addProp(object, 'id', props);
}
console.log(JSON.stringify(objArr, null, 2));
I have this sample data:
const data = [
{
id: 1,
title: 'Sports',
menus: [
{
id: 2,
title: 'Basketball',
menus: [
{
id: 3,
title: 'NBA',
},
{
id: 4,
title: 'NCAA',
},
{
id: 5,
title: 'G-League',
},
],
},
],
},
{
id: 100,
title: 'Names',
menus: [],
},
];
I want to change all the menus keys into children, so the result would be:
const result = [
{
id: 1,
title: 'Sports',
children: [
{
id: 2,
title: 'Basketball',
children: [
{
id: 3,
title: 'NBA',
},
{
id: 4,
title: 'NCAA',
},
{
id: 5,
title: 'G-League',
},
],
},
],
},
{
id: 100,
title: 'Names',
children: [],
},
];
I'm trying with this code:
const replacer = { menus: 'children' };
const transform = useCallback(
(obj) => {
if (obj && Object.getPrototypeOf(obj) === Object.prototype) {
return Object.fromEntries(Object.entries(obj).map(([k, v]) => [replacer[k] || k, transform(v)]));
}
return obj;
},
[replacer]
);
but it only changes the keys at the first level. How can I make it work?
You can use a recursive function that makes use of destructuring:
const replaceKey = arr =>
arr.map(({menus, ...o}) =>
menus ? {...o, children: replaceKey(menus)} : o);
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: [],},];
console.log(replaceKey(data));
To provide the old/new key dynamically, use the following variant:
const replaceKey = (arr, source, target) =>
arr.map(({[source]: v, ...o}) =>
v ? {...o, [target]: replaceKey(v, source, target)} : o);
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: [],},];
console.log(replaceKey(data, "menus", "children"));
This code assumes that values for the given key are arrays. If for some reason their values could be something else, then the code needs a bit more extension:
const data = [{id: 1,title: 'Sports',menus: [{id: 2,title: 'Basketball',menus: [{id: 3,title: 'NBA',},{id: 4,title: 'NCAA',},{id: 5,title: 'G-League',},],},],},{id: 100,title: 'Names',menus: 13,},];
const replaceKey = (arr, source, target) =>
Array.isArray(arr) ? arr.map(({[source]: value, ...o}) =>
value !== undefined ? {...o, [target]: replaceKey(value, source, target)} : o
) : arr;
console.log(replaceKey(data, "menus", "children"));
To see the effect of this code, the value for the very last menus key was changed to 13.
If the object is not big:
let data=[{id:1,title:'Sports',menus:[{id:2,title:'Basketball',menus:[{id:3,title:'NBA',},{id:4,title:'NCAA',},{id:5,title:'G-League',},],},],},{id:100,title:'Names',menus:[],},];
data = JSON.parse(JSON.stringify(data).replace(/"menus"\:/g,'"children":'))
console.log(data)
check this package: paix: that's take original source object and desired keys replacement then return a new object with desired keys, ex:
npm i paix
import { paix } from 'paix';
const data = [
{
id: 1,
title: 'Sports',
menus: [
{
id: 2,
title: 'Basketball',
menus: [
{
id: 3,
title: 'NBA',
},
],
},
],
},
{
id: 100,
title: 'Names',
menus: [],
},
];
const keys_swap = {menus: "children"};
const result = data.map(i => paix(i, keys_swap));
So first, here's a simple snippet to demonstrate what I mean exactly, and what I have tried.
let array_1 = [
{ id: 1, name: 'Peter' },
{ id: 2, name: 'John' },
{ id: 3, name: 'Andrew' },
{ id: 4, name: 'Patrick' },
{ id: 5, name: 'Brian' }
];
let array_2 = [
{ id: 1, name: 'not Peter' },
{ id: 80, name: 'not John' },
{ id: 3, name: 'not Andrew' },
{ id: 40, name: 'not Patrick' },
{ id: 5, name: 'not Brian' }
];
array_1.forEach(item_1 => {
for (let i = 0; i < array_2.length; i++) {
item_1.matches = array_2[i].id === item_1.id
}
});
console.log('matched_array', array_1);
The goal here is to add the matches property to each object in array_1 and set it to true/false, based on whether the id matches with any other id from array_2.
In this current example, the result of the matches properties should go like this: true - false - true - false - true. But my current code only sets this property correctly in the last element of the array (array_1).
Obviously it's because my code is not entirely correct, and that's where I'm stuck.
You could first create one object with reduce method that you can then use as a hash table to check if the element with the same id exists in the array 2.
let array_1=[{"id":1,"name":"Peter"},{"id":2,"name":"John"},{"id":3,"name":"Andrew"},{"id":4,"name":"Patrick"},{"id":5,"name":"Brian"}, {"id":6,"name":"Joe"}]
let array_2=[{"id":1,"name":"not Peter"},{"id":80,"name":"not John"},{"id":3,"name":"not Andrew"},{"id":40,"name":"not Patrick"},{"id":5,"name":"not Brian"}]
const o = array_2.reduce((r, e) => (r[e.id] = true, r), {})
const result = array_1.map(e => ({ ...e, matches: o[e.id] || false}))
console.log(result)
I would first collect the ids of array_2 in a Set, sets have a O(1) lookup time so checking if an id is in this set is fast. Then iterate over array_1 and check if the id is present in the created set using has().
let array_1 = [
{ id: 1, name: 'Peter' },
{ id: 2, name: 'John' },
{ id: 3, name: 'Andrew' },
{ id: 4, name: 'Patrick' },
{ id: 5, name: 'Brian' }
];
let array_2 = [
{ id: 1, name: 'not Peter' },
{ id: 80, name: 'not John' },
{ id: 3, name: 'not Andrew' },
{ id: 40, name: 'not Patrick' },
{ id: 5, name: 'not Brian' }
];
const array_2_ids = new Set(array_2.map(item_2 => item_2.id));
array_1.forEach(item_1 => item_1.matches = array_2_ids.has(item_1.id));
console.log('matched_array', array_1);
Your current code doesn't work because the for-loop will update the item_1.matches property for each element in array_2. This means you are overwriting the property each time. This in turn will effectivly result in item_1 only being checked against the last item in array_2.
To make your code work this:
array_1.forEach(item_1 => {
for (let i = 0; i < array_2.length; i++) {
item_1.matches = array_2[i].id === item_1.id
}
});
Should be changed into this:
array_1.forEach(item_1 => {
for (let i = 0; i < array_2.length; i++) {
if (array_2[i].id === item_1.id) {
item_1.matches = true;
return;
}
}
item_1.matches = false;
});
I have array of multiple objects. Every object have a key "id". I want to merge objects On the basis of same value of "id".
var obj = [{'id': 1, 'title': 'Hi'}, {'id': 1, 'description': 'buddy'}, {'id': 2, 'title': 'come'}, {'id': 2, 'description': 'On'}]
And i want output something like that
var new_obj = [{'id': 1, 'title': 'Hi' 'description': 'buddy'}, {id: 2, 'title': 'come', 'description': 'on'}]
This way works :
let toMerge = [{'id': 1, 'title': 'Hi'}, {'id': 1, 'description': 'buddy'}, {'id': 2, 'title': 'come'}, {'id': 2, 'description': 'On'}]
let merged = []
for ( let object of toMerge ) {
// if object not already merged
if(!merged.find(o => o.id === object.id)) {
// filter the same id's objects, merge them, then push the merged object in merged array
merged.push(toMerge.filter(o => o.id === object.id).reduce((acc, val) => {return {...acc, ...val}}))
}
}
You could reduce the array by finding an object with the same id and assign the object to found or take a new object.
var array = [{ id: 1, title: 'Hi' }, { id: 1, description: 'buddy' }, { id: 2, title: 'come' }, { id: 2, description: 'On' }],
result = array.reduce((r, o) => {
var temp = r.find(({ id }) => o.id === id);
if (!temp) r.push(temp = {});
Object.assign(temp, o);
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Here's a solution
const data = [
{
id: 1,
title: "Hi"
},
{
description: "buddy",
id: 1
},
{
id: 2,
title: "come"
},
{
description: "On",
id: 2
}
]
const f = (data) =>
Object.values(data.reduce(
(y, x) => ({
...y,
[x.id]: {
...x,
...(y[x.id] || {})
},
}),
{},
))
console.log(f(data))
Here's a solution using Ramda.js
const data = [
{
id: 1,
title: "Hi"
},
{
description: "buddy",
id: 1
},
{
id: 2,
title: "come"
},
{
description: "On",
id: 2
}
]
const f = R.compose(R.values, R.reduceBy(R.merge, {}, R.prop('id')))
console.log(f(data))
<script src="//cdn.jsdelivr.net/npm/ramda#latest/dist/ramda.min.js"></script>
Say I have an array of objects that follows the pattern below:
var posts = [
{
title: post_ab,
category_array : [
{ id: 1, slug: category-a },
{ id: 2, slug: category-b }
]
},
{
title: post_ac,
category_array : [
{ id: 1, slug: category-a },
{ id: 3, slug: category-c }
]
},
{
title: post_bc,
category_array : [
{ id: 2, slug: category-b },
{ id: 3, slug: category-c }
]
}
]
I'm trying to filter the above array, and only return values where the category_array contains a slug matching a specified value.
For example, if I wanted to filter against 'category-c', only the 2nd and 3rd values (post_ac and post_bc) would be returned.
I've tried with nested filters, which is getting me nowhere:
var currentCategory = 'category-b';
var filteredPosts = function( posts ) {
return posts.filter( function( post ){
return post.category_array.filter( function( category ){
return category.slug === currentCategory;
})
})
}
You have to use Array.prototype.some() in the inner loop:
var filteredPosts = function(posts) {
return posts.filter(function(post){
return post["category_array"].some(function(category){
return category.slug === currentCategory;
});
});
}
It will return a boolean result that can be used in the .filter() callback.
Let's combine some with filter
var posts = [
{
title: 'post_ab',
category_array : [
{ id: 1, slug: 'category-a' },
{ id: 2, slug: 'category-b' }
]
},
{
title: 'post_ac',
category_array : [
{ id: 1, slug: 'category-a' },
{ id: 3, slug: 'category-c' }
]
},
{
title: 'post_bc',
category_array : [
{ id: 2, slug: 'category-b' },
{ id: 3, slug: 'category-c' }
]
}
];
var result = posts.filter(a => a.category_array.some(cat => cat.slug.includes('a')));
console.log(result);
You can use Array.prototype.some
posts.filter(
(elem) => (
elem.category_array.some(
elem => elem.slug === currentCategory;
)
)
)
You can make a Array.prototype.find() to get first element with slug equal to 'category-b' in the array category_array and returning that element if exists will be evaluated as truthy, or falsey if not exist, by the Array.prototype.filter():
var posts = [{title: 'post_ab',category_array : [{ id: 1, slug: 'category-a' },{ id: 2, slug: 'category-b' }]},{title: 'post_ac',category_array : [{ id: 1, slug: 'category-a' },{ id: 3, slug: 'category-c' }]},{title: 'post_bc',category_array : [{ id: 2, slug: 'category-b' },{ id: 3, slug: 'category-c' }]}],
currentCategory = 'category-b',
result = posts.filter(function (p) {
// Return only the very first element found
return p.category_array.find(function (c) {
return currentCategory === c.slug;
});
});
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Replace your inner filter with some()