Recursively Add Elements To Array Based on Length - javascript

I'm trying to create a recursive function that will return a list of strings representing every attribute for a given schema. It must merge this with the attributes for a given document, to include elements for each time an array element occurs in the document.
For example. If you run the following code, I expect the result to include friends.1.addresses.1.country. However it does not. I think this is due to the fact that it's not recursively including all the array element possibilities. Since only 0 is set for the other array possibilities in the parentKey & postKey variables.
Any ideas how to fix this?
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
console.log(main()); // Should print `friends.1.addresses.1.country` as one of the elements in the array but does not.
function main() {
const result = schemaAttributes.reduce((accumulator, currentValue) => {
accumulator.push(currentValue);
const attributeParts = currentValue.split(".");
attributeParts.forEach((a, index) => {
const isLastPartNumber = !isNaN(parseInt(a));
if (isLastPartNumber) {
const parentKey = attributeParts.slice(0, index).join(".");
const postKey = attributeParts.slice(index + 1).join(".");
const numberOfItems = get(myDocument, parentKey).length;
for (let i = 1; i < numberOfItems; i++) {
accumulator.push([parentKey, i, postKey].filter((a) => Boolean(a)).join("."));
}
}
});
return accumulator;
}, []);
return [...new Set(result)];
}
function get(object, key) {
const keyParts = key.split(".");
let returnValue = object;
keyParts.forEach((part) => {
if (returnValue) {
returnValue = returnValue[part];
}
});
return returnValue;
}
Expected Result (order does not matter):
[
"id",
"friends",
"friends.0",
"friends.1",
"friends.0.name",
"friends.1.name",
"friends.0.addresses",
"friends.1.addresses",
"friends.0.addresses.0",
"friends.1.addresses.0",
"friends.1.addresses.1",
"friends.0.addresses.0.country",
"friends.1.addresses.0.country",
"friends.1.addresses.1.country",
"friends.0.addresses.0.zip",
"friends.1.addresses.0.zip",
"friends.1.addresses.1.zip"
]

Below traversing all paths matching your schemaAttribute, assuming a zero in your schemaAttribute is a wildcard for the position in array.
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
const out = new Set()
schemaAttributes.forEach(attr => {
out.add(attr)
traverse(myDocument, attr.split('.'), 0, [], path => out.add(path.join('.')))
})
function traverse (node, attrPath, idxAttrPath, outPath, cb) {
if (idxAttrPath === attrPath.length) {
return cb(outPath)
}
if (!node) { // can not explore further
return
}
const attr = attrPath[idxAttrPath]
if (attr === '0') {
if (!Array.isArray(node)) { // can not explore further
return
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], attrPath, idxAttrPath + 1, outPath, cb)
outPath.pop()
})
} else {
outPath.push(attr)
traverse(node[attr], attrPath, idxAttrPath + 1, outPath, cb)
outPath.pop()
}
}
console.log('out', [...out].sort((a, b) => a.localeCompare(b)))
An alternative (in the spirit more efficient) would be to consider a trie so we don't explore every schemaAttribute from the start. A 'nice' property being that the fields are printed in order and we don't have to sort as done in the first approach (although it does not matter to you)
Note that the traverse function is almost identical
Note2: Notice the cb(outPath) done for every traversal, not only for leaves.
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
function f2 (schemaAttributes, doc) {
// build a tree out of schema attributes
const root = {}
schemaAttributes.forEach(sa => {
node = root
sa.split('.').forEach(attr => {
node[attr] = node[attr] || {}
node = node[attr]
})
})
// explore the tree
function traverse (node, treeNode, outPath, cb) {
cb(outPath)
if (Object.keys(treeNode).length === 0) { // a leaf
return // cb(outPath)
}
if (!node) {
return
}
Object.keys(treeNode).forEach(attr => {
if (attr === '0') {
if (!Array.isArray(node)) { // can not explore further
return
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], treeNode[attr], outPath, cb)
outPath.pop()
})
} else {
outPath.push(attr)
traverse(node[attr], treeNode[attr], outPath, cb)
outPath.pop()
}
})
}
const out = []
traverse(doc, root, [], p => out.push(p.join('.')))
return out.slice(1) // discard the empty string
}
console.log(f2(schemaAttributes, myDocument))
Regarding the presence of friends.2.addresses.0.zip the underlying idea is that the path to the leaf should be present even if path in the document is at some point undefined.
So the adaptation is to fake the path on the document so we can continue traversing it up until the tree leaf is reached
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip',
'bob.moran.everywhere' // for properties as well
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
},
{
"name": "Pumba",
"addresses": [] // empty addresses! should show ..friends.2.adresses.0....
}
]
};
function f3 (schemaAttributes, doc) {
// build a tree out of schema attributes
const root = {}
schemaAttributes.forEach(sa => {
node = root
sa.split('.').forEach(attr => {
node[attr] = node[attr] || {}
node = node[attr]
})
})
// explore the tree
function traverse (node, treeNode, outPath, cb, virtualPath) {
cb(outPath)
if (Object.keys(treeNode).length === 0) { // a leaf
return //cb(outPath)
}
Object.keys(treeNode).forEach(attr => {
if (attr === '0') {
if (!node || node.length == 0) {
node = [{}] // fake the path for arrays
}
node.forEach((n, i) => {
outPath.push(i)
traverse(node[i], treeNode[attr], outPath, cb)
outPath.pop()
})
} else {
if (!node) { // fake the path for properties
node = {}
}
outPath.push(attr)
traverse(node[attr], treeNode[attr], outPath, cb)
outPath.pop()
}
})
}
const out = []
traverse(doc, root, [], p => out.push(p.join('.')))
return out.slice(1)
}
console.log(f3(schemaAttributes, myDocument))

You could use try out this piece of code:
const schemaAttributes = [
'id',
'friends',
'friends.0',
'friends.0.name',
'friends.0.addresses',
'friends.0.addresses.0',
'friends.0.addresses.0.country',
'friends.0.addresses.0.zip'
];
const myDocument = {
"id": 1,
"friends": [
{
"name": "Bob",
"addresses": [
{"country": "world"}
]
},
{
"name": "Tim",
"addresses": [
{"country": "moon"},
{"zip": 12345}
]
}
]
};
main(); // Should print `friends.1.addresses.1.country` as one of the elements in the array but does not.
function extractKeysRecursively(obj) {
const attributes = Object.keys(obj)
return attributes
flatMap(key =>
typeof obj[key] === 'object'
? [isNaN(key) && key, ...extractKeysRecursively(obj[key], ( ( parentKey && parentKey + '.' ) || '' ) + key )]
: ( ( parentKey && parentKey + '.' ) || '' ) + key
)
.filter(key => !!key)
}
function main() {
console.log(schemaAttributes.concat(extractKeysRecursively(myDocument)))
}
Lemme know you need some extra help (:

Related

Having issues with deleting object from an array where match is found

I have the follow function that will delete object from array. It also returns the array tree without the items that was deleted. My issues is that it works when my objToFindBy is null deleting everything where {group: null} is found however it error with promise rejection if I set objToFindBy {group: 'some string'}
This code should delete all occurrences where the objToFindBy is a match, example {group: null} will find everywhere will the group is null and delete all object and then return the full tree without the objects that was deleted
findAndDeleteAll(tree, 'items', {group: null}) // work and delete all where match. then returns the tree without deleted objects
findAndDeleteAll(tree, 'items', {group: 'd575c91f-4765-4073-a948-5e305116610c'}) // promise rejection
const tree ={
"type": "app",
"info": "Custom Layout",
"items": [
{
"id": "d575c91f-4765-4073-a948-5e305116610c",
"title": "Fc",
"group": null
},
{
"id": "890d5a1e-3f03-42cd-a695-64a17b6b9bea",
"title": null,
"group": null
},
{
"id": "cbe00537-0bb8-4837-8019-de48cb04edd6",
"title": null,
"group": "d575c91f-4765-4073-a948-5e305116610c",
},
{
"id": "b8751c32-2121-4907-a229-95e3e49bcb39",
"title": null,
"group": "d575c91f-4765-4073-a948-5e305116610c"
}
],
"Children": []
}
var findAndDeleteAll = function findAndDeleteAll(tree, childrenKey, objToFindBy) {
var treeModified = false;
var findKeys = Object.keys(objToFindBy);
var findSuccess = false;
findKeys.forEach(function (key) {
(0, _lodash2.default)(tree[key], objToFindBy[key]) ? findSuccess = true : findSuccess = false;
});
if (findSuccess) {
Object.keys(tree).forEach(function (key) {
return delete tree[key];
});
return tree;
}
function innerFunc(tree, childrenKey, objToFindBy) {
if (tree[childrenKey]) {
var _loop = function _loop(index) {
var findKeys = Object.keys(objToFindBy);
var findSuccess = false;
findKeys.forEach(function (key) {
(0, _lodash2.default)(tree[childrenKey][index][key], objToFindBy[key]) ? findSuccess = true : findSuccess = false;
});
if (findSuccess) {
tree[childrenKey].splice(index, 1);
treeModified = true;
}
if (tree[childrenKey][index].hasOwnProperty(childrenKey)) {
innerFunc(tree[childrenKey][index], childrenKey, objToFindBy);
}
};
for (var index = tree[childrenKey].length - 1; index >= 0; index--) {
_loop(index);
}
}
}
innerFunc(tree, childrenKey, objToFindBy);
return treeModified ? tree : false;
};
how about shorter solution?
const findAndDeleteAll = (tree, childrenKey, nestedKey, nestedValue) => {
return{...tree, [childrenKey]: tree[childrenKey].filter((row) => {
return row[nestedKey] !== nestedValue;
})}
}
const a = findAndDeleteAll(tree, 'items', 'group', null) // work and delete all where match. then returns the tree without deleted objects
const b = findAndDeleteAll(tree, 'items', 'group', 'd575c91f-4765-4073-a948-5e305116610c') // promise rejection
console.warn(a);
console.warn(b);
Your function would be so much simper, reusable, better, if you send not the redundant tree - but instead deleteFrom(tree.items, "group", null);. think about it.
const deleteFrom = (arr, pr, val) => arr.filter(ob => ob[pr] !== val);
const tree = {
type: "app",
info: "Custom Layout",
items: [
{ id: "10c", title: "Fc", group: null },
{ id: "bea", title: null, group: null },
{ id: "dd6", title: null, group: "10c" },
{ id: "b39", title: null, group: "10c" },
],
Children: []
};
const items = deleteFrom(tree.items, "group", null);
console.log(items); // Only the filtered items array
const newTree = {...tree, items};
console.log(newTree); // Your brand new tree!

Merge array of arrays based sub array index as keys (NodeJS/Javascript)

How to write a code that would merge my list in the following way? Performance is important. I want to convert the following array:
"list": [
[
"marketing",
"page_sections",
"PageOne"
],
[
"marketing",
"page_sections",
"PageTwo"
],
[
"webapp",
"page",
"pageone"
],
[
"webapp",
"page",
"pagetwo"
],
To the following format:
[
{
name: "marketing",
path: "marketing/",
children: [
{
name: "page_sections",
path: "marketing/page_sections",
children: [
{
name: "pageOne",
path: "marketing/page_sections/pageOne",
children: []
},
{
name: "pageTwo",
path: "marketing/page_sections/pageTwo",
children: []
},
}
],
},
{
name: "webapp",
path: "webapp/"
children: [
{
name: "page",
path: "webapp/page/"
children: [
{
name: "pageone",
path: "webapp/page/pageone"
children: []
},
{
name: "pagetwo",
path: "webapp/page/pagetwo"
children: []
},
}
]
},
]
The first index of sub array is parent, second index is child of parent, third index is child of second index (and so on).
The shortest approach is to iterate the nested names and look for an object with the same name. If not exist, create a new object. Return the children array as new level.
This approach features Array#reduce for iterating the outer array of data and for all inner arrays.
const
data = [["marketing", "page_sections", "PageOne"], ["marketing", "page_sections", "PageTwo"], ["webapp", "page", "pageone"], ["webapp", "page", "pagetwo"]],
result = data.reduce((r, names) => {
names.reduce((level, name, i, values) => {
let temp = level.find(q => q.name === name),
path = values.slice(0, i + 1).join('/') + (i ? '' : '/');
if (!temp) level.push(temp = { name, path, children: [] });
return temp.children;
}, r);
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Looking at the source, and your expected result.
What I would do is loop the list, and then do another loop inside the list. Mix this with Array.find..
Eg..
const data = {list:[
["marketing","page_sections","PageOne"],
["marketing","page_sections","PageTwo"],
["webapp","page","pageone"],
["webapp","page","pagetwo"]]};
function makeTree(src) {
const root = [];
for (const s of src) {
let r = root;
let path = '';
for (const name of s) {
path += `${name}/`;
let f = r.find(k => k.name === name);
if (!f) r.push(f = {name, path, children: []});
r = f.children;
}
}
return root;
}
console.log(makeTree(data.list));
.as-console-wrapper {
min-height: 100%;
}
You can do the following,
list= [
[
"marketing",
"page_sections",
"PageOne"
],
[
"marketing",
"page_sections",
"PageTwo"
],
[
"webapp",
"page",
"pageone"
],
[
"webapp",
"page",
"pagetwo"
],
];
getChildrenItem = (arr) => {
if(arr.length === 1) {
return { name: arr[0], children: []};
} else {
return { name: arr.splice(0,1)[0], children: [getChildrenItem([...arr])]};
}
}
merge = (srcArr, newObj) => {
const {name, children} = newObj;
let index = srcArr.findIndex(item => item.name === name);
if( index> -1) {
children.forEach(item => merge(srcArr[index].children, item))
return ;
} else {
srcArr.push(newObj);
return;
}
}
allObj = [];
list.forEach(item => {
let tempObj = getChildrenItem([...item]);
merge(allObj, tempObj);
});
console.log(allObj);
If Performance is an issue, I think this is one of the best solutions.
let list = [
["marketing", "page_sections", "PageOne"],
["marketing", "page_sections", "PageTwo"],
["webapp", "page", "pageone"],
["webapp", "page", "pagetwo"],
];
const dt = {};
const pushToOBJ = (Object, name) => {
if (Object[name]) return Object[name];
Object[name] = {
name,
children: {},
};
return Object[name];
};
for (let i = 0; i < list.length; i++) {
let subArray = list[i];
let st = pushToOBJ(dt, subArray[0]);
for (let j = 1; j < subArray.length; j++) {
st = pushToOBJ(st.children, subArray[j]);
}
}
let result = [];
const convertObjToChildArray = (obj) => {
if (obj === {}) return [];
let arr = Object.values(obj);
for (let i = 0; i < arr.length; i++) {
arr[i].children = convertObjToChildArray(arr[i].children);
}
return arr;
};
result = convertObjToChildArray(dt);
console.log(result);
No Use of JS find function, which already has O(n) Complexity.

Creating nested array from flat array - Data Structure

I need to create a nested array using the path as reference for the children.
E.g: 4.1 is a child of 4, 4.1.1 is a child of 4.1, 4.2 is a child of 4...
I have this flat array, with all the data and paths. How would be the best approach to create a nested array where the children are nested to its parent based on its path.
Input:
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
Output:
const list = [
{
location: 1,
path: '4',
children: [
{
location: 2,
path: '4.1',
children: [
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
]
},
{
location: 5,
path: '4.2',
children: [
{
location: 6,
path: '4.2.1'
},
]
},
{
location: 7,
path: '4.3',
children: [
{
location: 8,
path: '4.3.1'
}
]
},
]
},
];
The best approach would be something recursive.
Any suggestions for this algorithm?
I was curious if the linked answer from Scott would be able to solve this problem without modification. It does!
import { tree } from './Tree'
import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const myTree =
tree // <- make tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
[
{
"location": 1,
"path": "4",
"children": [
{
"location": 2,
"path": "4.1",
"children": [
{
"location": 3,
"path": "4.1.1",
"children": []
},
{
"location": 4,
"path": "4.1.2",
"children": []
}
]
},
{
"location": 5,
"path": "4.2",
"children": [
{
"location": 6,
"path": "4.2.1",
"children": []
}
]
},
{
"location": 7,
"path": "4.3",
"children": [
{
"location": 8,
"path": "4.3.1",
"children": []
}
]
}
]
}
]
The Tree module is shared in this post and here's a peek at the Func module that supplies bind -
// Func.js
const identity = x => x
const bind = (f, ...args) =>
f(...args)
const raise = (msg = "") => // functional throw
{ throw Error(msg) }
// ...
export { identity, bind, raise, ... }
Expand the snippet below to verify the results in your browser -
// Func.js
const bind = (f, ...args) =>
f(...args)
// Index.js
const empty = _ =>
new Map
const update = (r, k, t) =>
r.set(k, t(r.get(k)))
const append = (r, k, v) =>
update(r, k, (all = []) => [...all, v])
const index = (all = [], indexer) =>
all.reduce
( (r, v) => append(r, indexer(v), v)
, empty()
)
// Tree.js
// import { index } from './Index'
function tree (all, indexer, maker, root = null)
{ const cache =
index(all, indexer)
const many = (all = []) =>
all.map(x => one(x))
const one = (single) =>
maker(single, next => many(cache.get(next)))
return many(cache.get(root))
}
// Main.js
// import { tree } from './Tree'
// import { bind } from './Func'
const parent = (path = "") =>
bind
( (pos = path.lastIndexOf(".")) =>
pos === -1
? null
: path.substr(0, pos)
)
const list =
[{location:1,path:'4'},{location:2,path:'4.1'},{location:3,path:'4.1.1'},{location:4,path:'4.1.2'},{location:5,path:'4.2'},{location:6,path:'4.2.1'},{location:7,path:'4.3'},{location:8,path:'4.3.1'}]
const myTree =
tree
( list // <- array of nodes
, node => parent(node.path) // <- foreign key
, (node, children) => // <- node reconstructor
({ ...node, children: children(node.path) }) // <- primary key
)
console.log(JSON.stringify(myTree, null, 2))
One way to do this is to use an intermediate index mapping paths to objects, then folding your list into a structure by looking up each node and its parent in the index. If there is no parent, then we add it to the root object. In the end, we return the children of our root object. Here's some code for that:
const restructure = (list) => {
const index = list .reduce(
(a, {path, ...rest}) => ({...a, [path]: {path, ...rest}}),
{}
)
return list .reduce((root, {path}) => {
const node = index [path]
const parent = index [path .split('.') .slice(0, -1) .join('.')] || root
parent.children = [...(parent.children || []), node]
return root
}, {children: []}) .children
}
const list = [{location: 1, path: '4'}, {location: 2, path: '4.1' }, {location: 3, path: '4.1.1'}, {location: 4, path: '4.1.2'}, {location: 5, path: '4.2'}, {location: 6, path: '4.2.1'}, {location: 7, path: '4.3'}, {location: 8, path: '4.3.1'}]
console.log (restructure (list))
.as-console-wrapper {min-height: 100% !important; top: 0}
Using the index means that we don't have to sort anything; the input can be in any order.
Finding the parent involves replacing, for instance, "4.3.1" with "4.3" and looking that up in the index. And when we try "4", it looks up the empty string, doesn't find it and uses the root node.
If you prefer regex, you could use this slightly shorter line instead:
const parent = index [path.replace (/(^|\.)[^.]+$/, '')] || root
But, you might also want to look at a more elegant technique in a recent answer on a similar question. My answer here, gets the job done (with a bit of ugly mutation) but that answer will teach you a lot about effective software development.
You can first sort the array of objects by path so that the parent will always be before it's children in the sorted array.
eg: '4' will be before '4.1'
Now, you can create an object where the keys are the paths. Let's assume '4' is already inserted in our object.
obj = {
'4': {
"location": 1,
"path": "4",
}
}
When we process '4.1', we first check if '4' is present in our object. If yes, we now go into its children (if the key 'children' isn't present, we create a new empty object) and check if '4.1' is present. If not, we insert '4.1'
obj = {
'4': {
"location": 1,
"path": "4",
"children": {
"4.1": {
"location": 2,
"path": "4.1"
}
}
}
}
We repeat this process for each element in list. Finally, we just have to recursively convert this object into an array of objects.
Final code:
list.sort(function(a, b) {
return a.path - b.path;
})
let obj = {}
list.forEach(x => {
let cur = obj;
for (let i = 0; i < x.path.length; i += 2) {
console.log(x.path.substring(0, i + 1))
if (x.path.substring(0, i + 1) in cur) {
cur = cur[x.path.substring(0, i + 1)]
if (!('children' in cur)) {
cur['children'] = {}
}
cur = cur['children']
} else {
break;
}
}
cur[x.path] = x;
})
function recurse (obj) {
let res = [];
Object.keys(obj).forEach((key) => {
if (obj[key]['children'] !== null && typeof obj[key]['children'] === 'object') {
obj[key]['children'] = recurse(obj[key]['children'])
}
res.push(obj[key])
})
return res;
}
console.log(recurse(obj));
was thinking along the same terms as Aadith but came up from an iterative approach. I think the most performant way to do it is to use a linked list structure and then flatten it though.
const list = [
{
location: 1,
path: '4'
},
{
location: 2,
path: '4.1'
},
{
location: 3,
path: '4.1.1'
},
{
location: 4,
path: '4.1.2'
},
{
location: 5,
path: '4.2'
},
{
location: 6,
path: '4.2.1'
},
{
location: 7,
path: '4.3'
},
{
location: 8,
path: '4.3.1'
}
];
let newList = [];
list.forEach((location) =>
{
console.log('Handling location ',location);
if(location.path.split('.').length==1)
{
location.children = [];
newList.push(location);
}
else
{
newList.forEach(loc => {
console.log('checking out: ',loc);
let found = false;
while(!found)
{
console.log(loc.path,'==',location.path.substring(0, location.path.lastIndexOf('.')));
found = loc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(!found)
{
for(let i=0;i<loc.children.length;i++)
{
let aloc = loc.children[i];
found = aloc.path == location.path.substring(0, location.path.lastIndexOf('.'));
if(found)
{
console.log('found it...', loc);
location.children = [];
aloc.children.push(location);
break;
}
}
}
else
{
console.log('found it...', loc);
location.children = [];
loc.children.push(location);
}
}
} );
}
});
console.log(newList);
This was my quick and dirty way of how to go about it
Thank you for all the suggestions!
I could definitely see really sophisticated solutions to my problem.
By the end of the day, I've ended up creating my own "dirty" solution.
It is definitely a slower approach, but for my application this list won't be long to the point where i should be too worry about optimization.
I had simplified the flatted list for the purpose of my question. Although, in reality the list was a little more complex then that. I could also pass the path I want to find its children.
This is the solution that worked for me.
function handleNested(list, location) {
return list.map((item) => {
if (item.children && item.children.length > 0) {
item.children = handleNested(item.children, location);
}
if (
location.path.startsWith(item.path) &&
location.path.split(".").length === item.path.split(".").length + 1
) {
return {
...item,
children: item.children ? [...item.children, location] : [location],
};
} else {
return item;
}
});
}
function locationList(path) {
// Filtering the list to remove items with different parent path, and sorting by path depthness.
const filteredList = list
.filter((location) => location.path.startsWith(path))
.sort((a, b) => a.path.length - b.path.length);
let nestedList = [];
// Looping through the filtered list and placing each item under its parent.
for (let i = 0; i < filteredList.length; i++) {
// Using a recursive function to find its parent.
let res = handleNested(nestedList, filteredList[i]);
nestedList = res;
}
return nestedList;
}
locationList("4");

Relate and merge array of same Department

I am working on an application where I need to get combine the object of same department based on the
conditions provided in the second Array and attach the relation to the object.
let inArr1 = [{"D1D2":"AND"},{"D3D4":"OR"}]
let inArr2 =[{"ID":"1","NAME":"KEN","DEPT1":"CSE"},
{"ID":"2","NAME":"MARK","DEPT2":"IT"},
{"ID":"3","NAME":"TOM","DEPT3":"ECE"},
{"ID":"4","NAME":"SHIV","DEPT4":"LIB"},
{"ID":"5","NAME":"TIM","DEPT5":"SEC"}
]
Output
outArr ={
[{"ID":"1","NAME":"KEN","DEPT1":"CSE","REL":"AND"},
{"ID":"2","NAME":"MARK","DEPT2":"IT","REL":"AND"}], //Arr1
[{"ID":"3","NAME":"TOM","DEPT3":"ECE","REL":"OR"},
{"ID":"4","NAME":"SHIV","DEPT4":"LIB","REL":"OR"}], //Arr2
[{"ID":"5","NAME":"TIM","DEPT5":"SEC"}] //Arr3
}
Code:
let condArr=[],outArr,i=1;
inArr1.forEach(condt => {
let dept = Object.keys(condt)[0];
let tmparr = dept.split("D");
tmparr.shift()
condArr.push(tmparr)
});
inArr2.forEach(condt => {
if(condArr.includes(inArr2.D+i)){
i++;
outArr.push(inArr2);
}
});
Your code has a bit confused logic, i would suggest rather this
let inArr1 = [{"D1D2":"AND"},{"D3D4":"OR"},{"D5D6":"AND"}]
let inArr2 =[{"ID":"1","NAME":"KEN","DEPT1":"CSE"},
{"ID":"2","NAME":"MARK","DEPT2":"IT"},
{"ID":"3","NAME":"TOM","DEPT3":"ECE"},
{"ID":"4","NAME":"SHIV","DEPT4":"LIB"},
{"ID":"5","NAME":"TIM","DEPT5":"SEC"},
{"ID":"6","NAME":"TLA","DEPT6":"SEC"},
]
// first lets create object of ids as keys and conditions as values
const [keys, conditions] = inArr1.reduce((agg, cond, index) => {
Object.entries(cond).forEach(([key, value]) => {
key.split('D').forEach(v => { if (v) agg[0][v] = { value, index }})
agg[1].push([])
})
return agg
}, [{}, []]) // {1: "AND", 2: "AND", 3: "OR", 4: "OR"}
conditions.push([])
// and now just map over all elements and add condition if we found id from the keys
inArr2.forEach(item => {
const cond = keys[item.ID]
if (cond) conditions[cond.index].push({...item, REL: cond.value})
else conditions[conditions.length - 1].push(item)
})
const res = conditions.filter(v => v.length)
console.log(res)
You could store the goups by using the ID and use new objects.
let inArr1 = [{ D1D2: "AND" }, { D3D4: "OR" }],
inArr2 = [{ ID: "1", NAME: "KEN", DEPT1: "CSE" }, { ID: "2", NAME: "MARK", DEPT2: "IT" }, { ID: "3", NAME: "TOM", DEPT3: "ECE" }, { ID: "4", NAME: "SHIV", DEPT4: "LIB" }, { ID: "5", NAME: "TIM", DEPT5: "SEC" }],
groups = inArr1.reduce((r, o) => {
Object.entries(o).forEach(([k, REL]) => {
var object = { REL, group: [] };
k.match(/[^D]+/g).forEach(id => r[id] = object);
});
return r;
}, {}),
grouped = inArr2.reduce((r, o) => {
var { REL, group } = groups[o.ID] || {};
if (group) {
if (!group.length) r.push(group);
group.push(Object.assign({}, o, { REL }));
} else {
r.push([o]);
}
return r;
}, []);
console.log(grouped);
.as-console-wrapper { max-height: 100% !important; top: 0; }
can try other solution:
let inArr1 = [{ D1D2: "AND" }, { D3D4: "OR" }, { D6D7: "XOR" }];
let inArr2 = [
{ ID: "1", NAME: "KEN", DEPT1: "CSE" },
{ ID: "2", NAME: "MARK", DEPT2: "IT" },
{ ID: "3", NAME: "TOM", DEPT3: "ECE" },
{ ID: "4", NAME: "SHIV", DEPT4: "LIB" },
{ ID: "5", NAME: "TIM", DEPT5: "SEC" },
{ ID: "9", NAME: "BAR", DEPT5: "XYZ" },
{ ID: "6", NAME: "FOO", DEPT5: "XYZ" },
];
let unmatchedArr = []
let matchedArr = inArr2.reduce((acc, obj) => {
// getting index matched from inArr1 objects key
const indexMatched = getIndexMatch(obj.ID);
// creating index if not exists
if (!acc[indexMatched] && indexMatched !== null) acc[indexMatched] = [];
// if some index matched it merge current obj with DEL property with inArr1[indexMatched] key => value
return indexMatched !== null
? acc[indexMatched].push({
...obj,
DEL: inArr1[indexMatched][Object.keys(inArr1[indexMatched])[0]]
})
// pushing on unmatchedArr
: unmatchedArr.push(obj)
, acc
}, []);
function getIndexMatch(id) {
for (const [index, obj] of inArr1.entries()) {
for (const key of Object.keys(obj)) {
// spliting only digits of the current key of object
if (key.match(/\d/g).includes(id)) return index; // returning index of inArr1 if is included
}
}
return null;
}
// merging arrays
const result = [...matchedArr, unmatchedArr];
console.log(result);

Convert paths with items to tree object

I'm trying to convert an array of object contains paths with item to tree of data so I wrote a function path loop on the path:
From this array:
[
{ userName: "1", tags: ["A;B"] },
{ userName: "2", tags: ["A;B"] },
{ userName: "3", tags: ["A;"] },
{ userName: "4", tags: ["A;B;C"] },
{ userName: "5", tags: ["A;B"] },
{ userName: "6", tags: ["A;B;C;D"] }
]
to this structure:
[{
name: "A",
families: [{
name: "B",
families: [{
name: "C",
families: [{
name: "D",
families: [],
items: ["6"]
}],
items: ["4"]
}],
items: ["1", "2", "5"]
}],
items: ["3"]
}]
function convertListToTree(associationList) {
let tree = [];
for (let i = 0; i < associationList.length; i++) {
let path = associationList[i].tags[0].split(';');
let assetName = associationList[i].userName;
let currentLevel = tree;
for (let j = 0; j < path.length; j++) {
let familyName = path[j];
let existingPath = findWhere(currentLevel, 'name', familyName);
if (existingPath) {
if (j === path.length - 1) {
existingPath.items.push(assetName);
}
currentLevel = existingPath.families;
} else {
let assets = [];
if (j === path.length - 1) {
assets.push(assetName)
}
let newPart = {
name: familyName,
families: [],
items: assets,
};
currentLevel.push(newPart);
currentLevel = newPart.families;
}
}
}
return tree;
}
function findWhere(array, key, value) {
let t = 0;
while (t < array.length && array[t][key] !== value) {
t++;
}
if (t < array.length) {
return array[t]
} else {
return false;
}
}
But I have some issue here that the expected output is not like I want
[
{
"name": "A",
"families": [
{
"name": "B",
"families": [
{
"name": "C",
"families": [
{
"name": "D",
"families": [],
"items": [
"6"
]
}
],
"items": [
"4"
]
}
],
"items": [
"1",
"2",
"5"
]
},
{
"name": "",
"families": [],
"items": [
"3"
]
}
],
"items": []
}
]
Can someone please help me to fix that
You should be able to use recursion to achieve this, using getFamilies and getUsers functions called at each level:
const allTags = ["A", "B", "C", "D"];
let a = [ { "userName": "1", "tags": ["A;B"] }, { "userName": "2", "tags": ["A;B"] }, { "userName": "3", "tags": ["A;"] }, { "userName": "4", "tags": ["A;B;C"] }, { "userName": "5", "tags": ["A;B"] }, { "userName": "6", "tags": ["A;B;C;D"] } ];
// This function assumes order is not important, if it is, remove the sort() calls.
function arraysEqual(a1, a2) {
return a1.length === a2.length && a1.sort().every(function(value, index) { return value === a2.sort()[index]});
}
function getUserNames(tags, arr) {
return arr.filter(v => arraysEqual(v.tags[0].split(';').filter(a => a),tags)).map(({userName}) => userName);
}
function getFamilies(tags) {
if (tags.length >= allTags.length) return [];
const name = allTags[tags.length];
const path = [...tags, name];
return [{ name, families: getFamilies(path), items: getUserNames(path, a)}];
}
let res = getFamilies([]);
console.log('Result:', JSON.stringify(res, null, 4));
The idea here is to iterate the data (the reduce loop), and whenever a node is missing from the Map (nodesMap), use createBranch to recursively create the node, create the parent (if needed...), and then assign the node to the parent, and so on. The last step is to get a unique list of root paths (A in your data), and extract them from the Map (tree) to an array.
const createBranch = ([name, ...tagsList], nodesMap, node) => {
if(!nodesMap.has(name)) { // create node if not in the Map
const node = { name, families: [], items: [] };
nodesMap.set(name, node);
// if not root of branch create the parent...
if(tagsList.length) createBranch(tagsList, nodesMap, node);
};
// if a parent assign the child to the parent's families
if(node) nodesMap.get(name).families.push(node);
};
const createTree = data => {
const tree = data.reduce((nodesMap, { userName: item, tags: [tags] }) => {
const tagsList = tags.match(/[^;]+/g).reverse(); // get all nodes in branch and reverse
const name = tagsList[0]; // get the leaf
if(!nodesMap.has(name)) createBranch(tagsList, nodesMap); // if the leaf doesn't exist create the entire branch
nodesMap.get(name).items.push(item); // assign the item to the leaf's items
return nodesMap;
}, new Map());
// get a list of uniqnue roots
const roots = [...new Set(data.map(({ tags: [tags] }) => tags.split(';')[0]))];
return roots.map(root => tree.get(root)); // get an array of root nodes
}
const data = [{"userName":"1","tags":["A;B"]},{"userName":"2","tags":["A;B"]},{"userName":"3","tags":["A;"]},{"userName":"4","tags":["A;B;C"]},{"userName":"5","tags":["A;B"]},{"userName":"6","tags":["A;B;C;D"]}];
const result = createTree(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
Allow me to make two small changes, and ramda's mergeDeepWithKey will do most of the work for you.
Changes, before we start:
Make tags an array rather than an array containing one string (i.e. tags[0].split(";"))
Allow families to be a dictionary-like object rather than an array (if you ever need your array format, it's Object.values(dict))
Solution:
Transform every entry to a path of the desired format using reduce
Merge all paths with custom logic:
When merging name entries, don't change the name
When merging items entries, concatenate
const inp = [
{ userName: "1", tags: ["A","B"] },
{ userName: "2", tags: ["A","B"] },
{ userName: "3", tags: ["A"] },
{ userName: "4", tags: ["A","B","C"] },
{ userName: "5", tags: ["A","B"] },
{ userName: "6", tags: ["A","B","C","D"] }
];
// Transform an input element to a nested path of the right format
const Path = ({ userName, tags }) => tags
.slice(0, -1)
.reduceRight(
(families, name) => ({ name, families: { [families.name]: families },
items: []
}),
({ name: last(tags), families: {}, items: [userName] })
);
// When merging path entries, use this custom logic
const mergePathEntry = (k, v1, v2) =>
k === "name" ? v1 :
k === "items" ? v1.concat(v2) :
null;
const result = inp
.map(Path)
// Watch out for inp.length < 2
.reduce(
mergeDeepWithKey(mergePathEntry)
)
console.log(JSON.stringify(result, null, 2));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const { mergeDeepWithKey, last } = R;</script>

Categories

Resources