Find object by property path in nested array of objects - javascript

I have such object:
var obj = [
{
name: 'ob_1',
childFields: [],
},
{
name: 'ob_2',
childFields: [
{
name: 'ob_2_1',
childFields: [
{
name: 'ob_3_1',
childFields: [],
test: 124
},
],
},
],
},
]
function getObjectByNamePath(path, fieds) {
const pathArr = path.split('.');
const result = fieds.find(field => {
if (pathArr.length > 1) {
if (field.name === pathArr[0] && field.childFields.length) {
const newPath = pathArr.slice(1, pathArr.length).join('.');
return getObjectByNamePath(newPath, field.childFields);
}
return false;
} else {
if (field.name === pathArr[0]) {
return true;
} else {
return false;
}
}
});
return result;
}
I want to get object by name values path:
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj))
I tried this, but it doesn't work correct and i feel that there is more elegant way to achieve what i want. Thanks.

You could iterate the childFields and find the name for this level, then take the next level name.
function getObjectByNamePath(path, array) {
return path
.split('.')
.reduce(
({ childFields = [] } = {}, name) => childFields.find(o => o.name === name),
{ childFields: array }
);
}
var obj = [{ name: 'ob_1', childFields: [], }, { name: 'ob_2', childFields: [ { name: 'ob_2_1', childFields: [ { name: 'ob_3_1', childFields: [], test: 124 }] }] }];
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj));
console.log(getObjectByNamePath('ob_1', obj));
console.log(getObjectByNamePath('foo.bar', obj));
.as-console-wrapper { max-height: 100% !important; top: 0; }

A recursive solution.
var obj = [{ name: 'ob_1', childFields: [], }, { name: 'ob_2', childFields: [ { name: 'ob_2_1', childFields: [ { name: 'ob_3_1', childFields: [], test: 124 }] }] }]
function getObjectByNamePath(path, obj) {
const [currentPath, ...restPaths] = path.split('.');
const nextObj = (obj.find(e => e.name === currentPath))
if(!restPaths.length) return nextObj;
return getObjectByNamePath(restPaths.join('.'), nextObj.childFields || []);
}
// Test Cases
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1', obj))
console.log(getObjectByNamePath('ob_2.ob_2_1.ob_3_1.fakePath', obj))
console.log(getObjectByNamePath('ob_1', obj))
console.log(getObjectByNamePath('', obj))

Related

How to return the difference between two Objects having array and return the difference value dynamically

Let's say I have two objects of arrays:
const newObj = {
description: "abcd",
type: "anything",
list: [
{ id: 1, name: "Peter" },
{ id: 2, name: "Cathenna" },
{ id: 3, name: "Chiyo" }
]
}
const oldObj = {
description: "wwew",
type: "anything",
list: [
{ id: 1, name: "Amara" },
{ id: 2, name: "shinzo" },
{ id: 3, name: "Chiyo" }
]
}
I want to find all the updated data in newObj objects. Ex, description of oldObj is updated to "abcd" in newObj and in list name of two objects has been updated. So, my expected output is:
const extractObjDiff = {
description: "abcd",
list: [
{ id: 1, name: "Peter" },
{ id: 2, name: "Cathenna" }
]
}
I have tried below code but it's not working for array list.
function extractObjDiff(newObj, oldObj) {
var r = {};
for (var prop in newObj) {
if (!oldObj.hasOwnProperty(prop)) {
if (Array.isArray(newObj)) {
r = newObj;
} else {
r[prop] = newObj[prop];
}
} else if (newObj[prop] === Object(newObj[prop])) {
var difference = newObj[prop].length !== oldObj[prop].length ? newObj[prop] : extractObjDiff(newObj[prop], oldObj[prop], []);
if (Object.keys(difference).length > 0) r[prop] = difference;
} else if (newObj[prop] !== oldObj[prop]) {
if (newObj[prop] === undefined)
r[prop] = 'undefined';
if (newObj[prop] === null)
r[prop] = null;
else if (typeof newObj[prop] === 'function')
r[prop] = 'function';
else if (typeof newObj[prop] === 'object')
r[prop] = 'object'
else if (Array.isArray(newObj))
r = newObj;
else
r[prop] = newObj[prop];
}
}
return r;
}
I have added 2 functions, first one give your expected output and second one give the every changes in the new object.
const newObj = {
description: "abcd",
type: "anything",
list: [
{ id: 1, name: "Peter" },
{ id: 2, name: "Cathenna" },
{ id: 3, name: "Chiyo" }
]
};
const oldObj = {
description: "wwew",
type: "anything",
list: [
{ id: 1, name: "Amara" },
{ id: 2, name: "shinzo" },
{ id: 3, name: "Chiyo" }
]
};
const findSubChanges = (oldObj, newObj) => {
let changes=[]
for (const i in oldObj) {
if (oldObj[i] instanceof Object) {
let isChange=false
for (const j in oldObj[i]){
if(oldObj[i][j] !== newObj[i][j]){
isChange=true
break
}
}
if (isChange){
changes[i] = newObj[i]
}
} else {
if (oldObj[i] !== newObj[i]) {
changes[i] = newObj[i];
}
}
}
return changes
};
const findChanges = (oldObj, newObj) => {
let changes={}
for (const i in oldObj) {
if (oldObj[i] instanceof Object) {
const change = findSubChanges(oldObj[i], newObj[i]);
if (change) {
changes[i] = change
}
} else {
if (oldObj[i] !== newObj[i]) {
changes[i] = newObj[i];
}
}
}
return changes
};
const extractObjDiff=findChanges(oldObj, newObj)
console.log(extractObjDiff);
const newObj = {
description: "abcd",
type: "anything",
list: [
{ id: 1, name: "Peter" },
{ id: 2, name: "Cathenna" },
{ id: 3, name: "Chiyo" }
]
};
const oldObj = {
description: "wwew",
type: "anything",
list: [
{ id: 1, name: "Amara" },
{ id: 2, name: "shinzo" },
{ id: 3, name: "Chiyo" }
]
};
const isArray = (item) => item instanceof Array;
const isJSONObject = (item) =>
item instanceof Object && !(item instanceof Array);
const findEveryChanges = (oldObj, newObj) => {
if (
(isArray(oldObj) && isArray(newObj)) ||
(isJSONObject(oldObj) && isJSONObject(newObj))
) {
let changes = isArray(oldObj) ? [] : {};
for (const i in oldObj) {
if (oldObj[i] instanceof Object) {
const change = findEveryChanges(oldObj[i], newObj[i]);
if (change) {
changes[i] = change;
}
} else {
if (oldObj[i] !== newObj[i]) {
changes[i] = newObj[i];
}
}
}
if (Object.keys(changes).length > 0) {
return changes;
}
return;
}
if (oldObj !== newObj) {
return newObj;
}
return;
};
console.log(findEveryChanges(oldObj, newObj));

Creating a Tree out of PathStrings

I have a similar problem to this (Get a tree like structure out of path string). I tried to use the provided solution but can not get it to work in Angular.
The idea is to the separate incoming path strings (see below) and add them to an object and display them as a tree.
pathStrings: string[] = [
"PathA/PathA_0",
"PathA/PathA_1",
"PathA/PathA_2/a",
"PathA/PathA_2/b",
"PathA/PathA_2/c"
];
let tree: Node[] = [];
for (let i = 0; i < this.pathStrings.length; i++) {
tree = this.addToTree(tree, this.pathStrings[i].split("/"));
}
addToTree(root: Node[], names: string[]) {
let i: number = 0;
if (names.length > 0) {
for (i = 0; i < root.length; i++) {
if (root[i].name == names[0]) {
//already in tree
break;
}
}
if (i == root.length) {
let x: Node = { name: names[0] };
root.push(x);
}
root[i].children = this.addToTree(root[i].children, names.slice(1));
}
return root;
}
The result is supposed to look like this:
const TREE_DATA: Node[] = [
{
name: "PathA",
children: [
{ name: "PathA_0" },
{ name: "PathA_1" },
{
name: "PathA_2",
children: [{ name: "a" }, { name: "b" }, { name: "c" }]
}
]
},
{
name: "PathB",
children: [
{ name: "PathB_0" },
{ name: "PathB_1", children: [{ name: "a" }, { name: "b" }] },
{
name: "PathC_2"
}
]
},
{
name: "PathC",
children: [
{ name: "PathB_0" },
{ name: "PathB_1", children: [{ name: "a" }, { name: "b" }] },
{
name: "PathC_2"
}
]
}
];
Here is the Stackblitz Link (https://stackblitz.com/edit/angular-h3btn5?file=src/app/tree-flat-overview-example.ts) to my intents.. Im trying for days now without success.. Thank you so much!!
In plain Javascript, you could reduce the array by using a recursive function for thesearching and assign then new child to a given node.
const
getTree = (node, names) => {
const name = names.shift();
let child = (node.children ??= []).find(q => q.name === name);
if (!child) node.children.push(child = { name });
if (names.length) getTree(child, names);
return node;
},
pathStrings = ["PathA/PathA_0", "PathA/PathA_1", "PathA/PathA_2/a", "PathA/PathA_2/b", "PathA/PathA_2/c"],
tree = pathStrings
.reduce((target, path) => getTree(target, path.split('/')), { children: [] })
.children;
console.log(tree);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Change value from an array

I have the next code:
const arr = [
{
name:'john',
cars:[
{audi:1},
{bmw:2}
]
},
{
name:'bill',
cars:[
{audi:10},
{bmw:0}
]
}
]
const arr1 = arr.map(i => {
if(i.name === 'john') {
return i.cars.map( a => {
return {
...i,
test:[2]
}
})
}
return i
})
console.log(arr1)
Here i want too loop through the array and for the first object to change the cars array, adding test:[2]. For this i used:
const arr1 = arr.map(i => {
if(i.name === 'john') {
return i.cars.map( a => {
return {
...i,
test:[2]
}
})
}
return i
})
The issue is that my code don't return what i want. I get the first object like:
0: Object
name: "john"
cars: Array[2]
test: 2
1: Object
name: "john"
cars: Array[2]
test: 2
but i need like this:
{
name:'john',
cars:[
{
audi:1,
test: [2],
},
{bmw:2}
]
},
How to solve my issue?
Since you only want to change the first item in the cars array, I don't think map is right - instead, just list the first changed car as an object literal inside an array, then spread the remaining cars into the array with .slice(1):
const arr = [
{
name:'john',
cars:[
{audi:1},
{bmw:2}
]
},
{
name:'bill',
cars:[
{audi:10},
{bmw:0}
]
}
]
const arr1 = arr.map(person => (
person.name !== 'john'
? person
: ({
name: person.name,
cars: [
{ ...person.cars[0], test: [2] },
...person.cars.slice(1)
]
})
));
console.log(arr1)
You could address the right position and add the wanted property.
const
data = [{ name: 'john', cars: [{ audi: 1 }, { bmw: 2 }] }, { name: 'bill', cars: [{ audi: 10 }, { bmw: 0 }] }],
add = { target: [0, 0], value: { test: [2] } }
result = data.map((o, i) => i === add.target[0]
? { ...o, cars: o.cars.map((p, j) => j === add.target[1]
? {... p, ...add.value }
: p)
}
: o);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Given the desired result, a non mapping solution may be viable?
const arr = [{
name: 'john',
cars: [{
audi: 1
},
{
bmw: 2
}
]
},
{
name: 'bill',
cars: [{
audi: 10
},
{
bmw: 0
}
]
}
];
// clone the initial arr
const arrModified = Object.assign([], arr);
// find John
const indexJohn = arrModified.findIndex(v => v.name === "john");
if (indexJohn > -1) {
// modify the desired value
arrModified[indexJohn].cars[0].test = [2];
}
console.log(arrModified[0].cars);
.as-console-wrapper { top: 0; max-height: 100% !important; }

Reduce json api document to camel case

I would like to reduce a json api standard document to a camel version. I was able to make a camelCase objects and single primitive types keys.
I would like the desired output to be every key to be camelCase. So in the example above first-name would turn into firstName, snake-case would turn into snakeCase. The problem I'm facing is how to approach an array of objects using the same recursive call I'm using right now.
import _ from "lodash";
const data = {
id: 1,
type: "user",
links: { self: "/movies/1" },
meta: { "is-saved": false },
"first-name": "Foo",
"last-name": "Bar",
locations: ["SF"],
actors: [
{ id: 1, type: "actor", name: "John", age: 80 },
{ id: 2, type: "actor", name: "Jenn", age: 40 }
],
awards: [
{
id: 4,
type: "Oscar",
links: ["asd"],
meta: ["bar"],
category: "Best director",
'snake_case': 'key should be snakeCase'
}
],
name: { id: 1, type: "name", title: "Stargate" }
};
const needsCamelCase = str => {
return str.indexOf("-") > -1 || str.indexOf("_") > -1;
};
const strToCamelCase = function(str) {
return str.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};
const toCamelCase = obj => {
Object.keys(obj).forEach(key => {
if (_.isPlainObject(obj[key])) {
return toCamelCase(obj[key]);
}
if (_.isArray(obj[key])) {
// console.log(obj[key]);
obj[key].forEach(element => {
console.log(element);
});
//obj[key].foreach(element_ => toCamelCase);
}
if (needsCamelCase(key)) {
obj[strToCamelCase(key)] = obj[key];
delete obj[key];
}
});
return obj;
};
// toCamelCase(data);
console.log(toCamelCase(data));
Here is a codesand box: https://codesandbox.io/s/javascript-l842u
the logic is quite simple: if it's an object, just call to toCamelCase, if it's an array, iterate over it to create a new array. If it's an array of object, transform it with toCamelCase, if it's an array of something else keep it as is.
The solution would probably look like this:
const _ = require('lodash');
const data = {
id: 1,
type: "user",
links: { self: "/movies/1" },
meta: { "is-saved": false },
"first-name": "Foo",
"last-name": "Bar",
locations: ["SF"],
actors: [
{ id: 1, type: "actor", name: "John", age: 80 },
{ id: 2, type: "actor", name: "Jenn", age: 40 }
],
awards: [
{
id: 4,
type: "Oscar",
links: ["asd"],
meta: ["bar"],
category: "Best director",
'snake_case': 'key should be snakeCase'
}
],
name: { id: 1, type: "name", title: "Stargate" }
};
const needsCamelCase = str => {
return str.indexOf("-") > -1 || str.indexOf("_") > -1;
};
const strToCamelCase = function(str) {
return str.replace(/^([A-Z])|[\s-_](\w)/g, function(match, p1, p2, offset) {
if (p2) return p2.toUpperCase();
return p1.toLowerCase();
});
};
const toCamelCase = obj => {
Object.keys(obj).forEach(key => {
const camelCasedKey = needsCamelCase(key) ? strToCamelCase(key) : key;
const value = obj[key];
delete obj[key];
obj[camelCasedKey] = value;
if (_.isPlainObject(value)) {
obj[camelCasedKey] = toCamelCase(value);
}
if (_.isArray(value)) {
obj[camelCasedKey] = value.map(item => {
if (_.isPlainObject(item)) {
return toCamelCase(item);
} else {
return item;
}
});
}
});
return obj;
};
// toCamelCase(data);
console.log(toCamelCase(data));

Javascript get full index of nested object

const items = [
{ id: 'item1',
children: [
{ id: 'item1-1',
children: [
{ id: 'item1-1-1' },
{ id: 'item1-1-2' },
{ id: 'item1-1-3'
children: [
{ id: 'item1-1-3-1'}
]
},
]
},
{ id: 'item1-2',
children: [
{ id: 'item1-2-1' }
]
}
]
},
{ id: 'item2' }
]
What I want to is like below,
function getFullDepthOfObject(){
...
}
getFullIndexOfObject('item1') =====> return '1'
getFullIndexOfObject('item1-2') =====> return '1-2'
getFullIndexOfObject('item1-1-1') =====> return '1-1-1'
getFullIndexOfObject('item1-1-2') =====> return '1-1-2'
getFullIndexOfObject('item2') ===> return '2'
I have struggled with this too much time, But I couldn't make it. I think I should stack each of parent index, But I don't know how to get its parent. Is there a way to do this?
Not parse of id string. Each id has randomic string. The id like item1-2 is for easier demonstration.
I think my way is too verbose...
I tried like ...
// Get Full Index of item1-1
// First, get the target's depth.
var depth = 0;
function getDepthOfId(object, id) {
var level;
if (object.id === id) return 1;
object.children && object.children.some(o => level = getDepthOfId(o, id));
return level && level + 1;
}
depth = getDepthOfId(items[0], 'item1-1');
console.log('depth === ', depth)
// Then, iterate recursively with length of depth.
var indexStacks = [];
function getNestedIndexOfId(obj, id, index) {
if (obj.id === id) {
indexStacks = [index, ...indexStacks]
return index;
}
if (obj.children) {
depth++;
obj.children.map((child, i) => {
getNestedIndexOfId(child, id, i)
})
}
}
// I can get the inner index, but I can't get its parent id.
// I don't know how to this..
function getParentId(obj, id){
// ...?
var parentId;
return parentId;
}
for(var i=0; i<depth; i++){
getNestedIndexOfId('...')
}
// full path will be
indexStacks.join('-')
const items = [
{ id: 'item1',
children: [
{ id: 'item1-1',
children: [
{ id: 'item1-1-1' },
{ id: 'item1-1-2' },
{ id: 'item1-1-3',
children: [
{ id: 'item1-1-3-1'}
]
}
]
},
{ id: 'item1-2',
children: [
{ id: 'item1-2-1' }
]
}
]
},
{ id: 'item2' }
];
const searchIt = (node, search, path = '', position = 0) => {
if (node.id && node.id === search) {return path !== '' ? `${path}-${position}` : position;}
if (!node.children) {return false}
const index = node.children.findIndex((x) => x.id && x.id === search);
if (index >= 0) {
return path !== '' ? `${path}-${index + 1}` : index + 1;
}
for (let i = 0; i < node.children.length; i++) {
const result = searchIt(node.children[i], search, path !== '' ? `${path}-${i+1}` : i + 1, i);
if (result){
return result;
}
}
return false;
};
console.log(searchIt({children: items}, 'item1-1'));
console.log(searchIt({children: items}, 'item1-1-1'));
console.log(searchIt({children: items}, 'item1-1-2'));
console.log(searchIt({children: items}, 'item1-1-3'));
console.log(searchIt({children: items}, 'item1-1-3-1'));
console.log(searchIt({children: items}, 'item1-2-1'));
console.log(searchIt({children: items}, 'item1-1-3-2'));
console.log(searchIt({children: items}, 'item1-2-2'));
console.log(searchIt({children: items}, 'item3'));
You could take an recursive and iterative approach. On found, the path is returned from the most inner object to the outer call of the function.
function getPath(array, id) {
var result;
array.some((o, i) => {
var temp;
if (o.id === id) return result = `${i + 1}`;
if (temp = getPath(o.children || [], id)) return result = `${i + 1}-${temp}`;
});
return result;
}
const items = [{ id: 'item1', children: [{ id: 'item1-1', children: [{ id: 'item1-1-1' }, { id: 'item1-1-2' }, { id: 'item1-1-3', children: [{ id: 'item1-1-3-1'}] }] }, { id: 'item1-2', children: [{ id: 'item1-2-1' }] }] }, { id: 'item2' }];
console.log(getPath(items, 'item1')); // '1'
console.log(getPath(items, 'item1-2')); // '1-2'
console.log(getPath(items, 'item1-1-1')); // '1-1-1'
console.log(getPath(items, 'item1-1-2')); // '1-1-2'
console.log(getPath(items, 'item2')); // '2'
You can solve this problem using recursion. I have edited my code block and made it into a testable snippet. I had to fix an error in your data (missing comma or something don't remember).
const items = [
{ id: 'itemA',
children: [
{ id: 'item1-1',
children: [
{ id: 'item1-1-1' },
{ id: 'item1-1-2' },
{ id: 'item1-1-3', children: [ { id: 'item1-1-3-1'} ] },
]
},
{ id: 'item1-2', children: [ { id: 'item1-2-1' } ] },
],
},
{ id: 'item2' }
];
const getItemLevel = (targetKey, item, depth = 0) => {
if (item.id === targetKey) return depth;
let foundLevel = null;
if (item.children) {
item.children.forEach((child) => {
if (foundLevel) return;
foundLevel = getItemLevel(targetKey, child, depth +1);
})
}
return foundLevel;
}
console.log(getItemLevel('item1-1-1', { id:'root', children: items }));
console.log(getItemLevel('item2', { id:'root', children: items }));
console.log(getItemLevel('item1-1-3-1', { id:'root', children: items }));
console.log(getItemLevel('keydoesnotexist', { id:'root', children: items }));
a simple way:
const recursiveFind = (arr, id, res = {indexes: [], found: false}) => {
if (!Array.isArray(arr)) return res
const index = arr.findIndex(e => e.id === id)
if (index < 0) {
for (let i = 0; i < arr.length; i++) {
res.indexes.push(i+1)
const childIndexes = recursiveFind(arr[i].children, id, res)
if (childIndexes.found){
return childIndexes
}
}
}
else {
res.found = true
res.indexes.push(index+1)
}
return res
}
recursiveFind(items, 'item1-1-2').indexes.join('-')
If it's ok to use Lodash+Deepdash, then:
let path;
_.eachDeep(items,(val,key,parent,context)=>{
if(path) return false;
if(val.id=='item1-1-2'){
path=context.path;
return false;
}
},{tree:true,pathFormat:'array'});
console.log(_(path).without('children').map(v=>parseInt(v)+1).join('-'));
Here is a codepen for this

Categories

Resources