Recursive JavaScript function to locate an object by its UID - javascript

I'm having an issue returning the element which has been found in this hierarchical tree.
For example, if my selected item is:
{
"UID": 49,
"GUID": "",
"LocationName": "Doctor Smith's Office",
"LocationType": {
"UID": 2,
"LocationTypeName": "Practice",
"Description": "other location"
}
}
I will match up the UID to the below array of objects.
{
UID: 2,
GUID: "",
LocationName: "USA",
ParentLocation: null,
subs: [{
UID: 42,
GUID: "",
LocationName: "New Jersey",
Description: "",
subs: [{
UID: 3,
GUID: "",
LocationName: "Essex County",
ParentLocation: null,
"subs":[
UID: 4,
LocationName: "Newark",
ParentLocation: 3,
"subs": [
{
"UID": 49,
"GUID": "",
"LocationName": "Doctor Smith's Office",
"LocationType": {
"UID": 2,
"LocationTypeName": "Practice",
"Description": "other location"
},
"subs": [
{
"HostID": 38,
"HostName": "Ocean Host",
}
]
}
]
]
}
]
}]
};
let foundItem = this.findInTreeView(this.treeviewData[0], node.selectedNode);
// find selected node in treeview nav
// param: data - the treeview dataset
// param: selected - the selected node to be searched for in param 'data'
findInTreeView(data: any, selected: any ) {
let found;
if (this.foundInTree(data, selected)) {
return data;
}
let elem;
let ary = data.subs;
for (var i=0; i < ary.length; i++) {
elem = ary[i];
if (this.foundInTree(elem, selected)) {
// *** PROBLEM: If func has return true, I want to return the 'elem' object.
return elem;
}
}
for (var i=0; i < ary.length; i++) {
elem = ary[i];
if (elem.subs !== undefined) {
// recurse subs array
let found = this.findInTreeView(elem, selected);
if (found) {
return elem;
}
}
}
//return elem;
}
foundInTree(treeItem, node) {
if (treeItem.UID === node.UID) {
return true;
}
else {
return false;
}
}

It would be far easier to use a recursive reduce function, like this:
const input={UID:2,GUID:"",LocationName:"USA",ParentLocation:null,subs:[{UID:42,GUID:"",LocationName:"New Jersey",Description:"",subs:[{UID:3,GUID:"",LocationName:"Essex County",ParentLocation:null,"subs":[{UID:4,LocationName:"Newark",ParentLocation:3,"subs":[{"UID":49,"GUID":"","LocationName":"Doctor Smith's Office","LocationType":{"UID":2,"LocationTypeName":"Practice","Description":"other location"},"subs":[{"HostID":38,"HostName":"Ocean Host",}]}]}]}]}]};
const findUIDObj = (uid, parent) => {
const { UID, subs } = parent;
if (UID === uid) {
const { subs, ...rest } = parent;
return rest;
}
if (subs) return subs.reduce((found, child) => found || findUIDObj(uid, child), null);
};
console.log(findUIDObj(49, input))

You could use an explicit function which searches for the wanted UID.
function find(array, UID) {
var object;
array.some(o => {
if (o.UID === UID) {
return object = o;
}
return object = find(o.subs, UID);
});
return object;
}
var object = { UID: 2, GUID: "", LocationName: "USA", ParentLocation: null, subs: [{ UID: 42, GUID: "", LocationName: "New Jersey", Description: "", subs: [{ UID: 3, GUID: "", LocationName: "Essex County", ParentLocation: null, subs: [{ UID: 4, LocationName: "Newark", ParentLocation: 3, subs: [{ UID: 49, GUID: "", LocationName: "Doctor Smith's Office", LocationType: { UID: 2, LocationTypeName: "Practice", Description: "other location" }, subs: [{ HostID: 38, HostName: "Ocean Host", }] }] }] }] }] };
console.log(find([object], 49));
.as-console-wrapper { max-height: 100% !important; top: 0; }

One way to do this is to write a fairly generic version of a tree-finding function, and then configure it for your specific problem. Here we choose to test by matching on a supplied UID, we descend into children by looking at the subs property, and we convert the result by stripping out the subs property:
const searchTreeDF = (kids, test, convert, node) => test(node) // depth-first search
? convert(node)
: (kids(node) || []).reduce(
(found, child) => found || searchTreeDF(kids, test, convert, child),
false
)
const subs = node => node.subs
const matchId = (uid) => (item) => item.UID === uid
const convert = ({subs, ...rest}) => ({...rest})
const findUid = (uid, tree) => searchTreeDF(subs, matchId(uid), convert, tree)
// ...
const tree = {"GUID": "", "LocationName": "USA", "ParentLocation": null, "UID": 2, "subs": [{"Description": "", "GUID": "", "LocationName": "New Jersey", "UID": 42, "subs": [{"GUID": "", "LocationName": "Essex County", "ParentLocation": null, "UID": 3, "subs": [{"LocationName": "Newark", "ParentLocation": 3, "UID": 4, "subs": [{"GUID": "", "LocationName": "Doctor Smith's Office", "LocationType": {"Description": "other location", "LocationTypeName": "Practice", "UID": 2}, "UID": 49, "subs": [{"HostID": 38, "HostName": "Ocean Host"}]}]}]}]}]}
console.log(findUid(49, tree))
But if we didn't want to pass in the UID directly, but instead wanted to pass in an element that has its own UID property, we could write
const matchElem = (elem) => (item) => elem.UID === item.UID
and then do this:
const findUid2 = (elem, tree) => searchTreeDF(subs, matchElem(elem), convert, tree)
// ...
findUid2({UID: 49}, tree)
Or if we wanted to not convert the result, and keep the subs property, we could just supply an identity function for convert:
const findUid = (uid, tree) => searchTreeDF(subs, matchId(uid), x => x, tree)
Or we could mix and match as we please. Also note that the configuration does not have to use named functions. We could just as easily write
const findUid = (uid, tree) => searchTreeDF(
node => node.subs || [],
(item) => item.UID === uid,
({subs, ...rest}) => ({...rest}),
tree
)
Generic functions are not always the right answer. But they can help separate out those things that change from the more basic algorithm we're writing. I think in this case it helps make things more maintainable.

Related

Javascript customize json data format [duplicate]

I have a complex json file that I have to handle with javascript to make it hierarchical, in order to later build a tree.
Every entry of the json has :
id : a unique id,
parentId : the id of the parent node (which is 0 if the node is a root of the tree)
level : the level of depth in the tree
The json data is already "ordered". I mean that an entry will have above itself a parent node or brother node, and under itself a child node or a brother node.
Input :
{
"People": [
{
"id": "12",
"parentId": "0",
"text": "Man",
"level": "1",
"children": null
},
{
"id": "6",
"parentId": "12",
"text": "Boy",
"level": "2",
"children": null
},
{
"id": "7",
"parentId": "12",
"text": "Other",
"level": "2",
"children": null
},
{
"id": "9",
"parentId": "0",
"text": "Woman",
"level": "1",
"children": null
},
{
"id": "11",
"parentId": "9",
"text": "Girl",
"level": "2",
"children": null
}
],
"Animals": [
{
"id": "5",
"parentId": "0",
"text": "Dog",
"level": "1",
"children": null
},
{
"id": "8",
"parentId": "5",
"text": "Puppy",
"level": "2",
"children": null
},
{
"id": "10",
"parentId": "13",
"text": "Cat",
"level": "1",
"children": null
},
{
"id": "14",
"parentId": "13",
"text": "Kitten",
"level": "2",
"children": null
},
]
}
Expected output :
{
"People": [
{
"id": "12",
"parentId": "0",
"text": "Man",
"level": "1",
"children": [
{
"id": "6",
"parentId": "12",
"text": "Boy",
"level": "2",
"children": null
},
{
"id": "7",
"parentId": "12",
"text": "Other",
"level": "2",
"children": null
}
]
},
{
"id": "9",
"parentId": "0",
"text": "Woman",
"level": "1",
"children":
{
"id": "11",
"parentId": "9",
"text": "Girl",
"level": "2",
"children": null
}
}
],
"Animals": [
{
"id": "5",
"parentId": "0",
"text": "Dog",
"level": "1",
"children":
{
"id": "8",
"parentId": "5",
"text": "Puppy",
"level": "2",
"children": null
}
},
{
"id": "10",
"parentId": "13",
"text": "Cat",
"level": "1",
"children":
{
"id": "14",
"parentId": "13",
"text": "Kitten",
"level": "2",
"children": null
}
}
]
}
There is an efficient solution if you use a map-lookup. If the parents always come before their children you can merge the two for-loops. It supports multiple roots. It gives an error on dangling branches, but can be modified to ignore them. It doesn't require a 3rd-party library. It's, as far as I can tell, the fastest solution.
function list_to_tree(list) {
var map = {}, node, roots = [], i;
for (i = 0; i < list.length; i += 1) {
map[list[i].id] = i; // initialize the map
list[i].children = []; // initialize the children
}
for (i = 0; i < list.length; i += 1) {
node = list[i];
if (node.parentId !== "0") {
// if you have dangling branches check that map[node.parentId] exists
list[map[node.parentId]].children.push(node);
} else {
roots.push(node);
}
}
return roots;
}
var entries = [{
"id": "12",
"parentId": "0",
"text": "Man",
"level": "1",
"children": null
},
{
"id": "6",
"parentId": "12",
"text": "Boy",
"level": "2",
"children": null
},
{
"id": "7",
"parentId": "12",
"text": "Other",
"level": "2",
"children": null
},
{
"id": "9",
"parentId": "0",
"text": "Woman",
"level": "1",
"children": null
},
{
"id": "11",
"parentId": "9",
"text": "Girl",
"level": "2",
"children": null
}
];
console.log(list_to_tree(entries));
If you're into complexity theory this solution is Θ(n log(n)). The recursive-filter solution is Θ(n^2) which can be a problem for large data sets.
( BONUS1 : NODES MAY or MAY NOT BE ORDERED )
( BONUS2 : NO 3RD PARTY LIBRARY NEEDED, PLAIN JS )
( BONUS3 : User "Elias Rabl" says this is the most performant solution, see his answer below )
Here it is:
const createDataTree = dataset => {
const hashTable = Object.create(null);
dataset.forEach(aData => hashTable[aData.ID] = {...aData, childNodes: []});
const dataTree = [];
dataset.forEach(aData => {
if(aData.parentID) hashTable[aData.parentID].childNodes.push(hashTable[aData.ID])
else dataTree.push(hashTable[aData.ID])
});
return dataTree;
};
Here is a test, it might help you to understand how the solution works :
it('creates a correct shape of dataTree', () => {
const dataSet = [{
"ID": 1,
"Phone": "(403) 125-2552",
"City": "Coevorden",
"Name": "Grady"
}, {
"ID": 2,
"parentID": 1,
"Phone": "(979) 486-1932",
"City": "Chełm",
"Name": "Scarlet"
}];
const expectedDataTree = [{
"ID": 1,
"Phone": "(403) 125-2552",
"City": "Coevorden",
"Name": "Grady",
childNodes: [{
"ID": 2,
"parentID": 1,
"Phone": "(979) 486-1932",
"City": "Chełm",
"Name": "Scarlet",
childNodes : []
}]
}];
expect(createDataTree(dataSet)).toEqual(expectedDataTree);
});
As mentioned by #Sander, #Halcyon`s answer assumes a pre-sorted array, the following does not. (It does however assume you have loaded underscore.js - though it could be written in vanilla javascript):
Code
// Example usage
var arr = [
{'id':1 ,'parentid' : 0},
{'id':2 ,'parentid' : 1},
{'id':3 ,'parentid' : 1},
{'id':4 ,'parentid' : 2},
{'id':5 ,'parentid' : 0},
{'id':6 ,'parentid' : 0},
{'id':7 ,'parentid' : 4}
];
unflatten = function( array, parent, tree ){
tree = typeof tree !== 'undefined' ? tree : [];
parent = typeof parent !== 'undefined' ? parent : { id: 0 };
var children = _.filter( array, function(child){ return child.parentid == parent.id; });
if( !_.isEmpty( children ) ){
if( parent.id == 0 ){
tree = children;
}else{
parent['children'] = children
}
_.each( children, function( child ){ unflatten( array, child ) } );
}
return tree;
}
tree = unflatten( arr );
document.body.innerHTML = "<pre>" + (JSON.stringify(tree, null, " "))
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.9.1/underscore-min.js"></script>
Requirements
It assumes the properties 'id' and 'parentid' indicate ID and parent ID respectively. There must be elements with parent ID 0, otherwise you get an empty array back. Orphaned elements and their descendants are 'lost'
http://jsfiddle.net/LkkwH/1/
Use this ES6 approach. Works like charm
// Data Set
// One top level comment
const comments = [{
id: 1,
parent_id: null
}, {
id: 2,
parent_id: 1
}, {
id: 3,
parent_id: 1
}, {
id: 4,
parent_id: 2
}, {
id: 5,
parent_id: 4
}];
const nest = (items, id = null, link = 'parent_id') =>
items
.filter(item => item[link] === id)
.map(item => ({ ...item, children: nest(items, item.id) }));
console.log(
nest(comments)
)
Had the same problem, but I could not be certain that the data was sorted or not. I could not use a 3rd party library so this is just vanilla Js; Input data can be taken from #Stephen's example;
var arr = [
{'id':1 ,'parentid' : 0},
{'id':4 ,'parentid' : 2},
{'id':3 ,'parentid' : 1},
{'id':5 ,'parentid' : 0},
{'id':6 ,'parentid' : 0},
{'id':2 ,'parentid' : 1},
{'id':7 ,'parentid' : 4},
{'id':8 ,'parentid' : 1}
];
function unflatten(arr) {
var tree = [],
mappedArr = {},
arrElem,
mappedElem;
// First map the nodes of the array to an object -> create a hash table.
for(var i = 0, len = arr.length; i < len; i++) {
arrElem = arr[i];
mappedArr[arrElem.id] = arrElem;
mappedArr[arrElem.id]['children'] = [];
}
for (var id in mappedArr) {
if (mappedArr.hasOwnProperty(id)) {
mappedElem = mappedArr[id];
// If the element is not at the root level, add it to its parent array of children.
if (mappedElem.parentid) {
mappedArr[mappedElem['parentid']]['children'].push(mappedElem);
}
// If the element is at the root level, add it to first level elements array.
else {
tree.push(mappedElem);
}
}
}
return tree;
}
var tree = unflatten(arr);
document.body.innerHTML = "<pre>" + (JSON.stringify(tree, null, " "))
JS Fiddle
Flat Array to Tree
a more simple function list-to-tree-lite
npm install list-to-tree-lite
listToTree(list)
source:
function listToTree(data, options) {
options = options || {};
var ID_KEY = options.idKey || 'id';
var PARENT_KEY = options.parentKey || 'parent';
var CHILDREN_KEY = options.childrenKey || 'children';
var tree = [],
childrenOf = {};
var item, id, parentId;
for (var i = 0, length = data.length; i < length; i++) {
item = data[i];
id = item[ID_KEY];
parentId = item[PARENT_KEY] || 0;
// every item may have children
childrenOf[id] = childrenOf[id] || [];
// init its children
item[CHILDREN_KEY] = childrenOf[id];
if (parentId != 0) {
// init its parent's children object
childrenOf[parentId] = childrenOf[parentId] || [];
// push it into its parent's children object
childrenOf[parentId].push(item);
} else {
tree.push(item);
}
};
return tree;
}
jsfiddle
You can handle this question with just two line coding:
_(flatArray).forEach(f=>
{f.nodes=_(flatArray).filter(g=>g.parentId==f.id).value();});
var resultArray=_(flatArray).filter(f=>f.parentId==null).value();
Test Online (see the browser console for created tree)
Requirements:
1- Install lodash 4 (a Javascript library for manipulating objects and collections with performant methods => like the Linq in c#) Lodash
2- A flatArray like below:
var flatArray=
[{
id:1,parentId:null,text:"parent1",nodes:[]
}
,{
id:2,parentId:null,text:"parent2",nodes:[]
}
,
{
id:3,parentId:1,text:"childId3Parent1",nodes:[]
}
,
{
id:4,parentId:1,text:"childId4Parent1",nodes:[]
}
,
{
id:5,parentId:2,text:"childId5Parent2",nodes:[]
}
,
{
id:6,parentId:2,text:"childId6Parent2",nodes:[]
}
,
{
id:7,parentId:3,text:"childId7Parent3",nodes:[]
}
,
{
id:8,parentId:5,text:"childId8Parent5",nodes:[]
}];
Thank Mr. Bakhshabadi
Good luck
It may be useful package list-to-tree
Install:
bower install list-to-tree --save
or
npm install list-to-tree --save
For example, have list:
var list = [
{
id: 1,
parent: 0
}, {
id: 2,
parent: 1
}, {
id: 3,
parent: 1
}, {
id: 4,
parent: 2
}, {
id: 5,
parent: 2
}, {
id: 6,
parent: 0
}, {
id: 7,
parent: 0
}, {
id: 8,
parent: 7
}, {
id: 9,
parent: 8
}, {
id: 10,
parent: 0
}
];
Use package list-to-tree:
var ltt = new LTT(list, {
key_id: 'id',
key_parent: 'parent'
});
var tree = ltt.GetTree();
Result:
[{
"id": 1,
"parent": 0,
"child": [
{
"id": 2,
"parent": 1,
"child": [
{
"id": 4,
"parent": 2
}, {
"id": 5, "parent": 2
}
]
},
{
"id": 3,
"parent": 1
}
]
}, {
"id": 6,
"parent": 0
}, {
"id": 7,
"parent": 0,
"child": [
{
"id": 8,
"parent": 7,
"child": [
{
"id": 9,
"parent": 8
}
]
}
]
}, {
"id": 10,
"parent": 0
}];
I've written a test script to evaluate the performance of the two most general solutions (meaning that the input does not have to be sorted beforehand and that the code does not depend on third party libraries), proposed by users shekhardtu (see answer) and FurkanO (see answer).
http://playcode.io/316025?tabs=console&script.js&output
FurkanO's solution seems to be the fastest.
/*
** performance test for https://stackoverflow.com/questions/18017869/build-tree-array-from-flat-array-in-javascript
*/
// Data Set (e.g. nested comments)
var comments = [{
id: 1,
parent_id: null
}, {
id: 2,
parent_id: 1
}, {
id: 3,
parent_id: 4
}, {
id: 4,
parent_id: null
}, {
id: 5,
parent_id: 4
}];
// add some random entries
let maxParentId = 10000;
for (let i=6; i<=maxParentId; i++)
{
let randVal = Math.floor((Math.random() * maxParentId) + 1);
comments.push({
id: i,
parent_id: (randVal % 200 === 0 ? null : randVal)
});
}
// solution from user "shekhardtu" (https://stackoverflow.com/a/55241491/5135171)
const nest = (items, id = null, link = 'parent_id') =>
items
.filter(item => item[link] === id)
.map(item => ({ ...item, children: nest(items, item.id) }));
;
// solution from user "FurkanO" (https://stackoverflow.com/a/40732240/5135171)
const createDataTree = dataset => {
let hashTable = Object.create(null)
dataset.forEach( aData => hashTable[aData.id] = { ...aData, children : [] } )
let dataTree = []
dataset.forEach( aData => {
if( aData.parent_id ) hashTable[aData.parent_id].children.push(hashTable[aData.id])
else dataTree.push(hashTable[aData.id])
} )
return dataTree
};
/*
** lets evaluate the timing for both methods
*/
let t0 = performance.now();
let createDataTreeResult = createDataTree(comments);
let t1 = performance.now();
console.log("Call to createDataTree took " + Math.floor(t1 - t0) + " milliseconds.");
t0 = performance.now();
let nestResult = nest(comments);
t1 = performance.now();
console.log("Call to nest took " + Math.floor(t1 - t0) + " milliseconds.");
//console.log(nestResult);
//console.log(createDataTreeResult);
// bad, but simple way of comparing object equality
console.log(JSON.stringify(nestResult)===JSON.stringify(createDataTreeResult));
After many tries I came up with this:
const arrayToTree = (arr, parent = 0) => arr .filter(item => item.parent === parent).map(child => ({ ...child, children: arrayToTree(arr, child.index) }));
const entries = [
{
index: 1,
parent: 0
},
{
index: 2,
parent: 1
},
{
index: 3,
parent: 2
},
{
index: 4,
parent: 2
},
{
index: 5,
parent: 4
},
{
index: 6,
parent: 5
},
{
index: 7,
parent: 6
},
{
index: 8,
parent: 7
},
{
index: 9,
parent: 8
},
{
index: 10,
parent: 9
},
{
index: 11,
parent: 7
},
{
index: 13,
parent: 11
},
{
index: 12,
parent: 0
}
];
const arrayToTree = (arr, parent = 0) => arr .filter(item => item.parent === parent) .map(child => ({ ...child, children: arrayToTree(arr, child.index) })); console.log(arrayToTree(entries));
UPDATE 2022
This is a proposal for unordered items. This function works with a single loop and with a hash table and collects all items with their id. If a root node is found, then the object is added to the result array.
const
getTree = (data, root) => {
const t = {};
data.forEach(o => ((t[o.parentId] ??= {}).children ??= []).push(Object.assign(t[o.id] ??= {}, o)));
return t[root].children;
},
data = { People: [{ id: "12", parentId: "0", text: "Man", level: "1", children: null }, { id: "6", parentId: "12", text: "Boy", level: "2", children: null }, { id: "7", parentId: "12", text: "Other", level: "2", children: null }, { id: "9", parentId: "0", text: "Woman", level: "1", children: null }, { id: "11", parentId: "9", text: "Girl", level: "2", children: null }], Animals: [{ id: "5", parentId: "0", text: "Dog", level: "1", children: null }, { id: "8", parentId: "5", text: "Puppy", level: "2", children: null }, { id: "10", parentId: "13", text: "Cat", level: "1", children: null }, { id: "14", parentId: "13", text: "Kitten", level: "2", children: null }] },
result = Object.fromEntries(Object
.entries(data)
.map(([k, v]) => [k, getTree(v, '0')])
);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I like #WilliamLeung's pure JavaScript solution, but sometimes you need to make changes in existing array to keep a reference to object.
function listToTree(data, options) {
options = options || {};
var ID_KEY = options.idKey || 'id';
var PARENT_KEY = options.parentKey || 'parent';
var CHILDREN_KEY = options.childrenKey || 'children';
var item, id, parentId;
var map = {};
for(var i = 0; i < data.length; i++ ) { // make cache
if(data[i][ID_KEY]){
map[data[i][ID_KEY]] = data[i];
data[i][CHILDREN_KEY] = [];
}
}
for (var i = 0; i < data.length; i++) {
if(data[i][PARENT_KEY]) { // is a child
if(map[data[i][PARENT_KEY]]) // for dirty data
{
map[data[i][PARENT_KEY]][CHILDREN_KEY].push(data[i]); // add child to parent
data.splice( i, 1 ); // remove from root
i--; // iterator correction
} else {
data[i][PARENT_KEY] = 0; // clean dirty data
}
}
};
return data;
}
Exapmle:
https://jsfiddle.net/kqw1qsf0/17/
Convert nodes Array to Tree
ES6 function to convert an Array of nodes (related by parent ID) - to a Tree structure:
/**
* Convert nodes list related by parent ID - to tree.
* #syntax getTree(nodesArray [, rootID [, propertyName]])
*
* #param {Array} arr Array of nodes
* #param {integer} id Defaults to 0
* #param {string} p Property name. Defaults to "parent_id"
* #returns {Object} Nodes tree
*/
const getTree = (arr, p = "parent_id") => arr.reduce((o, n) => {
if (!o[n.id]) o[n.id] = {};
if (!o[n[p]]) o[n[p]] = {};
if (!o[n[p]].nodes) o[n[p]].nodes= [];
if (o[n.id].nodes) n.nodes= o[n.id].nodes;
o[n[p]].nodes.push(n);
o[n.id] = n;
return o;
}, {});
Generate HTML List from nodes Tree
Having our Tree in place, here's a recursive function to build the UL > LI Elements:
/**
* Convert Tree structure to UL>LI and append to Element
* #syntax getTree(treeArray [, TargetElement [, onLICreatedCallback ]])
*
* #param {Array} tree Tree array of nodes
* #param {Element} el HTMLElement to insert into
* #param {function} cb Callback function called on every LI creation
*/
const treeToHTML = (tree, el, cb) => el.append(tree.reduce((ul, n) => {
const li = document.createElement('li');
if (cb) cb.call(li, n);
if (n.nodes?.length) treeToHTML(n.nodes, li, cb);
ul.append(li);
return ul;
}, document.createElement('ul')));
Demo time
Here's an example having a linear Array of nodes and using both the above functions:
const getTree = (arr, p = "parent_id") => arr.reduce((o, n) => {
if (!o[n.id]) o[n.id] = {};
if (!o[n[p]]) o[n[p]] = {};
if (!o[n[p]].nodes) o[n[p]].nodes = [];
if (o[n.id].nodes) n.nodes = o[n.id].nodes;
o[n[p]].nodes.push(n);
o[n.id] = n;
return o;
}, {});
const treeToHTML = (tree, el, cb) => el.append(tree.reduce((ul, n) => {
const li = document.createElement('li');
if (cb) cb.call(li, n);
if (n.nodes?.length) treeToHTML(n.nodes, li, cb);
ul.append(li);
return ul;
}, document.createElement('ul')));
// DEMO TIME:
const nodesList = [
{id: 10, parent_id: 4, text: "Item 10"}, // PS: Order does not matters
{id: 1, parent_id: 0, text: "Item 1"},
{id: 4, parent_id: 0, text: "Item 4"},
{id: 3, parent_id: 5, text: "Item 3"},
{id: 5, parent_id: 4, text: "Item 5"},
{id: 2, parent_id: 1, text: "Item 2"},
];
const myTree = getTree(nodesList)[0].nodes; // Get nodes of Root (0)
treeToHTML(myTree, document.querySelector("#tree"), function(node) {
this.textContent = `(${node.parent_id} ${node.id}) ${node.text}`;
this._node = node;
this.addEventListener('click', clickHandler);
});
function clickHandler(ev) {
if (ev.target !== this) return;
console.clear();
console.log(this._node.id);
};
<div id="tree"></div>
Array elements can be in a chaotic order
let array = [
{ id: 1, data: 'something', parent_id: null, children: [] },
{ id: 2, data: 'something', parent_id: 1, children: [] },
{ id: 5, data: 'something', parent_id: 4, children: [] },
{ id: 4, data: 'something', parent_id: 3, children: [] },
{ id: 3, data: 'something', parent_id: null, children: [] },
{ id: 6, data: 'something', parent_id: null, children: [] }
]
function buildTree(array) {
let tree = []
for (let i = 0; i < array.length; i++) {
if (array[i].parent_id) {
let parent = array.filter(elem => elem.id === array[i].parent_id).pop()
parent.children.push(array[i])
} else {
tree.push(array[i])
}
}
return tree
}
const tree = buildTree(array)
console.log(tree);
.as-console-wrapper { min-height: 100% }
var data = [{"country":"india","gender":"male","type":"lower","class":"X"},
{"country":"china","gender":"female","type":"upper"},
{"country":"india","gender":"female","type":"lower"},
{"country":"india","gender":"female","type":"upper"}];
var seq = ["country","type","gender","class"];
var treeData = createHieArr(data,seq);
console.log(treeData)
function createHieArr(data,seq){
var hieObj = createHieobj(data,seq,0),
hieArr = convertToHieArr(hieObj,"Top Level");
return [{"name": "Top Level", "parent": "null",
"children" : hieArr}]
function convertToHieArr(eachObj,parent){
var arr = [];
for(var i in eachObj){
arr.push({"name":i,"parent":parent,"children":convertToHieArr(eachObj[i],i)})
}
return arr;
}
function createHieobj(data,seq,ind){
var s = seq[ind];
if(s == undefined){
return [];
}
var childObj = {};
for(var ele of data){
if(ele[s] != undefined){
if(childObj[ele[s]] == undefined){
childObj[ele[s]] = [];
}
childObj[ele[s]].push(ele);
}
}
ind = ind+1;
for(var ch in childObj){
childObj[ch] = createHieobj(childObj[ch],seq,ind)
}
return childObj;
}
}
this is what i used in a react project
// ListToTree.js
import _filter from 'lodash/filter';
import _map from 'lodash/map';
export default (arr, parentIdKey) => _map(_filter(arr, ar => !ar[parentIdKey]), ar => ({
...ar,
children: _filter(arr, { [parentIdKey]: ar.id }),
}));
usage:
// somewhere.js
import ListToTree from '../Transforms/ListToTree';
const arr = [
{
"id":"Bci6XhCLZKPXZMUztm1R",
"name":"Sith"
},
{
"id":"C3D71CMmASiR6FfDPlEy",
"name":"Luke",
"parentCategoryId":"ltatOlEkHdVPf49ACCMc"
},
{
"id":"aS8Ag1BQqxkO6iWBFnsf",
"name":"Obi Wan",
"parentCategoryId":"ltatOlEkHdVPf49ACCMc"
},
{
"id":"ltatOlEkHdVPf49ACCMc",
"name":"Jedi"
},
{
"id":"pw3CNdNhnbuxhPar6nOP",
"name":"Palpatine",
"parentCategoryId":"Bci6XhCLZKPXZMUztm1R"
}
];
const response = ListToTree(arr, 'parentCategoryId');
output:
[
{
"id":"Bci6XhCLZKPXZMUztm1R",
"name":"Sith",
"children":[
{
"id":"pw3CNdNhnbuxhPar6nOP",
"name":"Palpatine",
"parentCategoryId":"Bci6XhCLZKPXZMUztm1R"
}
]
},
{
"id":"ltatOlEkHdVPf49ACCMc",
"name":"Jedi",
"children":[
{
"id":"C3D71CMmASiR6FfDPlEy",
"name":"Luke",
"parentCategoryId":"ltatOlEkHdVPf49ACCMc"
},
{
"id":"aS8Ag1BQqxkO6iWBFnsf",
"name":"Obi Wan",
"parentCategoryId":"ltatOlEkHdVPf49ACCMc"
}
]
}
]```
I had similar issue couple days ago when have to display folder tree from flat array. I didn't see any solution in TypeScript here so I hope it will be helpful.
In my cases main parent were only one, also rawData array don't have to be sorted. Solutions base on prepare temp object like
{parentId: [child1, child2, ...] }
example raw data
const flatData: any[] = Folder.ofCollection([
{id: '1', title: 'some title' },
{id: '2', title: 'some title', parentId: 1 },
{id: '3', title: 'some title', parentId: 7 },
{id: '4', title: 'some title', parentId: 1 },
{id: '5', title: 'some title', parentId: 2 },
{id: '6', title: 'some title', parentId: 5 },
{id: '7', title: 'some title', parentId: 5 },
]);
def of Folder
export default class Folder {
public static of(data: any): Folder {
return new Folder(data);
}
public static ofCollection(objects: any[] = []): Folder[] {
return objects.map((obj) => new Folder(obj));
}
public id: string;
public parentId: string | null;
public title: string;
public children: Folder[];
constructor(data: any = {}) {
this.id = data.id;
this.parentId = data.parentId || null;
this.title = data.title;
this.children = data.children || [];
}
}
SOLUTION: Function that returns tree structure for flat argument
public getTree(flatData: any[]): Folder[] {
const addChildren = (item: Folder) => {
item.children = tempChild[item.id] || [];
if (item.children.length) {
item.children.forEach((child: Folder) => {
addChildren(child);
});
}
};
const tempChild: any = {};
flatData.forEach((item: Folder) => {
const parentId = item.parentId || 0;
Array.isArray(tempChild[parentId]) ? tempChild[parentId].push(item) : (tempChild[parentId] = [item]);
});
const tree: Folder[] = tempChild[0];
tree.forEach((base: Folder) => {
addChildren(base);
});
return tree;
}
I wrote an ES6 version based on #Halcyon answer
const array = [
{
id: '12',
parentId: '0',
text: 'one-1'
},
{
id: '6',
parentId: '12',
text: 'one-1-6'
},
{
id: '7',
parentId: '12',
text: 'one-1-7'
},
{
id: '9',
parentId: '0',
text: 'one-2'
},
{
id: '11',
parentId: '9',
text: 'one-2-11'
}
];
// Prevent changes to the original data
const arrayCopy = array.map(item => ({ ...item }));
const listToTree = list => {
const map = {};
const roots = [];
list.forEach((v, i) => {
map[v.id] = i;
list[i].children = [];
});
list.forEach(v => (v.parentId !== '0' ? list[map[v.parentId]].children.push(v) : roots.push(v)));
return roots;
};
console.log(listToTree(arrayCopy));
The principle of this algorithm is to use "map" to establish an index relationship. It is easy to find "item" in the list by "parentId", and add "children" to each "item", because "list" is a reference relationship, so "roots" will Build relationships with the entire tree.
Based on #FurkanO's answer, I created another version that does not mutate the origial data (like #Dac0d3r requested). I really liked #shekhardtu's answer, but realized it had to filter through the data many times. I thought a solution could be to use FurkanO's answer by copying the data first. I tried my version in jsperf, and the results where unfortunately (very) bleak... It seems like the accepted answer is really a good one! My version is quite configurable and failsafe though, so I share it with you guys anyway; here is my contribution:
function unflat(data, options = {}) {
const { id, parentId, childrenKey } = {
id: "id",
parentId: "parentId",
childrenKey: "children",
...options
};
const copiesById = data.reduce(
(copies, datum) => ((copies[datum[id]] = datum) && copies),
{}
);
return Object.values(copiesById).reduce(
(root, datum) => {
if ( datum[parentId] && copiesById[datum[parentId]] ) {
copiesById[datum[parentId]][childrenKey] = [ ...copiesById[datum[parentId]][childrenKey], datum ];
} else {
root = [ ...root, datum ];
}
return root
}, []
);
}
const data = [
{
"account": "10",
"name": "Konto 10",
"parentAccount": null
},{
"account": "1010",
"name": "Konto 1010",
"parentAccount": "10"
},{
"account": "10101",
"name": "Konto 10101",
"parentAccount": "1010"
},{
"account": "10102",
"name": "Konto 10102",
"parentAccount": "1010"
},{
"account": "10103",
"name": "Konto 10103",
"parentAccount": "1010"
},{
"account": "20",
"name": "Konto 20",
"parentAccount": null
},{
"account": "2020",
"name": "Konto 2020",
"parentAccount": "20"
},{
"account": "20201",
"name": "Konto 20201",
"parentAccount": "2020"
},{
"account": "20202",
"name": "Konto 20202",
"parentAccount": "2020"
}
];
const options = {
id: "account",
parentId: "parentAccount",
childrenKey: "children"
};
console.log(
"Hierarchical tree",
unflat(data, options)
);
With the options parameter, it is possible to configure what property to use as id or parent id. It is also possible to configure the name of the children property, if someone wants "childNodes": [] or something.
OP could simply use default options:
input.People = unflat(input.People);
If the parent id is falsy (null, undefined or other falsy values) or the parent object does not exist, we consider the object to be a root node.
My solution:
Allows bi-directional mapping (root to leaves and leaves to root)
Returns all nodes, roots, and leaves
One data pass and very fast performance
Vanilla Javascript
/**
*
* #param data items array
* #param idKey item's id key (e.g., item.id)
* #param parentIdKey item's key that points to parent (e.g., item.parentId)
* #param noParentValue item's parent value when root (e.g., item.parentId === noParentValue => item is root)
* #param bidirectional should parent reference be added
*/
function flatToTree(data, idKey, parentIdKey, noParentValue = null, bidirectional = true) {
const nodes = {}, roots = {}, leaves = {};
// iterate over all data items
for (const i of data) {
// add item as a node and possibly as a leaf
if (nodes[i[idKey]]) { // already seen this item when child was found first
// add all of the item's data and found children
nodes[i[idKey]] = Object.assign(nodes[i[idKey]], i);
} else { // never seen this item
// add to the nodes map
nodes[i[idKey]] = Object.assign({ $children: []}, i);
// assume it's a leaf for now
leaves[i[idKey]] = nodes[i[idKey]];
}
// put the item as a child in parent item and possibly as a root
if (i[parentIdKey] !== noParentValue) { // item has a parent
if (nodes[i[parentIdKey]]) { // parent already exist as a node
// add as a child
(nodes[i[parentIdKey]].$children || []).push( nodes[i[idKey]] );
} else { // parent wasn't seen yet
// add a "dummy" parent to the nodes map and put the item as its child
nodes[i[parentIdKey]] = { $children: [ nodes[i[idKey]] ] };
}
if (bidirectional) {
// link to the parent
nodes[i[idKey]].$parent = nodes[i[parentIdKey]];
}
// item is definitely not a leaf
delete leaves[i[parentIdKey]];
} else { // this is a root item
roots[i[idKey]] = nodes[i[idKey]];
}
}
return {roots, nodes, leaves};
}
Usage example:
const data = [{id: 2, parentId: 0}, {id: 1, parentId: 2} /*, ... */];
const { nodes, roots, leaves } = flatToTree(data, 'id', 'parentId', 0);
ES6 Map version :
getTreeData = (items) => {
if (items && items.length > 0) {
const data = [];
const map = {};
items.map((item) => {
const id = item.id; // custom id selector !!!
if (!map.hasOwnProperty(id)) {
// in case of duplicates
map[id] = {
...item,
children: [],
};
}
});
for (const id in map) {
if (map.hasOwnProperty(id)) {
let mappedElem = [];
mappedElem = map[id];
/// parentId : use custom id selector for parent
if (
mappedElem.parentId &&
typeof map[mappedElem.parentId] !== "undefined"
) {
map[mappedElem.parentId].children.push(mappedElem);
} else {
data.push(mappedElem);
}
}
}
return data;
}
return [];
};
/// use like this :
const treeData = getTreeData(flatList);
Incase anyone needs it for multiple parent. Refer id 2 which has multiple parents
const dataSet = [{
"ID": 1,
"Phone": "(403) 125-2552",
"City": "Coevorden",
"Name": "Grady"
},
{"ID": 2,
"Phone": "(403) 125-2552",
"City": "Coevorden",
"Name": "Grady"
},
{
"ID": 3,
"parentID": [1,2],
"Phone": "(979) 486-1932",
"City": "Chełm",
"Name": "Scarlet"
}];
const expectedDataTree = [
{
"ID":1,
"Phone":"(403) 125-2552",
"City":"Coevorden",
"Name":"Grady",
"childNodes":[{
"ID":2,
"parentID":[1,3],
"Phone":"(979) 486-1932",
"City":"Chełm",
"Name":"Scarlet",
"childNodes":[]
}]
},
{
"ID":3,
"parentID":[],
"Phone":"(403) 125-2552",
"City":"Coevorden",
"Name":"Grady",
"childNodes":[
{
"ID":2,
"parentID":[1,3],
"Phone":"(979) 486-1932",
"City":"Chełm",
"Name":"Scarlet",
"childNodes":[]
}
]
}
];
const createDataTree = dataset => {
const hashTable = Object.create(null);
dataset.forEach(aData => hashTable[aData.ID] = {...aData, childNodes: []});
const dataTree = [];
dataset.forEach(Datae => {
if (Datae.parentID && Datae.parentID.length > 0) {
Datae.parentID.forEach( aData => {
hashTable[aData].childNodes.push(hashTable[Datae.ID])
});
}
else{
dataTree.push(hashTable[Datae.ID])
}
});
return dataTree;
};
window.alert(JSON.stringify(createDataTree(dataSet)));
I used #FurkanO answer and made a generic function that can be used with any object type, I also wrote this function in TypeScript which i love it more because of auto completions.
Implementation:
1. Javascript:
export const flatListToTree = (flatList, idPath, parentIdPath, childListPath, isParent) => {
const rootParents = [];
const map = {};
for (const item of flatList) {
if (!item[childListPath]) item[childListPath] = [];
map[item[idPath]] = item;
}
for (const item of flatList) {
const parentId = item[parentIdPath];
if (isParent(item)) {
rootParents.push(item);
} else {
const parentItem = map[parentId];
parentItem[childListPath].push(item);
}
}
return rootParents;
};
2. TypeScript: I've assumed the "T" type has a property for children List, you can change 'childListPath' to be a string instead of "keyof T" if you have different use case.
export const flatListToTree = <T>(
flatList: T[],
idPath: keyof T,
parentIdPath: keyof T,
childListPath: keyof T,
isParent: (t: T) => boolean,
) => {
const rootParents: T[] = [];
const map: any = {};
for (const item of flatList) {
if (!(item as any)[childListPath]) (item as any)[childListPath] = [];
map[item[idPath]] = item;
}
for (const item of flatList) {
const parentId = item[parentIdPath];
if (isParent(item)) {
rootParents.push(item);
} else {
const parentItem = map[parentId];
parentItem[childListPath].push(item);
}
}
return rootParents;
};
How to use:
const nodes = [
{ id: 2, pid: undefined, children: [] },
{ id: 3, pid: 2 },
{ id: 4, pid: 2 },
{ id: 5, pid: 4 },
{ id: 6, pid: 5 },
{ id: 7, pid: undefined },
{ id: 8, pid: 7 },
];
const result = flatListToTree(nodes, "id", "pid", "children", node => node.pid === undefined);
Here's a simple helper function that I created modeled after the above answers, tailored to a Babel environment:
import { isEmpty } from 'lodash'
export default function unflattenEntities(entities, parent = {id: null}, tree = []) {
let children = entities.filter( entity => entity.parent_id == parent.id)
if (!isEmpty( children )) {
if ( parent.id == null ) {
tree = children
} else {
parent['children'] = children
}
children.map( child => unflattenEntities( entities, child ) )
}
return tree
}
also do it with lodashjs(v4.x)
function buildTree(arr){
var a=_.keyBy(arr, 'id')
return _
.chain(arr)
.groupBy('parentId')
.forEach(function(v,k){
k!='0' && (a[k].children=(a[k].children||[]).concat(v));
})
.result('0')
.value();
}
Here is a modified version of Steven Harris' that is plain ES5 and returns an object keyed on the id rather than returning an array of nodes at both the top level and for the children.
unflattenToObject = function(array, parent) {
var tree = {};
parent = typeof parent !== 'undefined' ? parent : {id: 0};
var childrenArray = array.filter(function(child) {
return child.parentid == parent.id;
});
if (childrenArray.length > 0) {
var childrenObject = {};
// Transform children into a hash/object keyed on token
childrenArray.forEach(function(child) {
childrenObject[child.id] = child;
});
if (parent.id == 0) {
tree = childrenObject;
} else {
parent['children'] = childrenObject;
}
childrenArray.forEach(function(child) {
unflattenToObject(array, child);
})
}
return tree;
};
var arr = [
{'id':1 ,'parentid': 0},
{'id':2 ,'parentid': 1},
{'id':3 ,'parentid': 1},
{'id':4 ,'parentid': 2},
{'id':5 ,'parentid': 0},
{'id':6 ,'parentid': 0},
{'id':7 ,'parentid': 4}
];
tree = unflattenToObject(arr);
This is a modified version of the above that works with multiple root items, I use GUIDs for my ids and parentIds so in the UI that creates them I hard code root items to something like 0000000-00000-00000-TREE-ROOT-ITEM
var tree = unflatten(records, "TREE-ROOT-ITEM");
function unflatten(records, rootCategoryId, parent, tree){
if(!_.isArray(tree)){
tree = [];
_.each(records, function(rec){
if(rec.parentId.indexOf(rootCategoryId)>=0){ // change this line to compare a root id
//if(rec.parentId == 0 || rec.parentId == null){ // example for 0 or null
var tmp = angular.copy(rec);
tmp.children = _.filter(records, function(r){
return r.parentId == tmp.id;
});
tree.push(tmp);
//console.log(tree);
_.each(tmp.children, function(child){
return unflatten(records, rootCategoryId, child, tree);
});
}
});
}
else{
if(parent){
parent.children = _.filter(records, function(r){
return r.parentId == parent.id;
});
_.each(parent.children, function(child){
return unflatten(records, rootCategoryId, child, tree);
});
}
}
return tree;
}
Copied from the Internet
http://jsfiddle.net/stywell/k9x2a3g6/
function list2tree(data, opt) {
opt = opt || {};
var KEY_ID = opt.key_id || 'ID';
var KEY_PARENT = opt.key_parent || 'FatherID';
var KEY_CHILD = opt.key_child || 'children';
var EMPTY_CHILDREN = opt.empty_children;
var ROOT_ID = opt.root_id || 0;
var MAP = opt.map || {};
function getNode(id) {
var node = []
for (var i = 0; i < data.length; i++) {
if (data[i][KEY_PARENT] == id) {
for (var k in MAP) {
data[i][k] = data[i][MAP[k]];
}
if (getNode(data[i][KEY_ID]) !== undefined) {
data[i][KEY_CHILD] = getNode(data[i][KEY_ID]);
} else {
if (EMPTY_CHILDREN === null) {
data[i][KEY_CHILD] = null;
} else if (JSON.stringify(EMPTY_CHILDREN) === '[]') {
data[i][KEY_CHILD] = [];
}
}
node.push(data[i]);
}
}
if (node.length == 0) {
return;
} else {
return node;
}
}
return getNode(ROOT_ID)
}
var opt = {
"key_id": "ID", //节点的ID
"key_parent": "FatherID", //节点的父级ID
"key_child": "children", //子节点的名称
"empty_children": [], //子节点为空时,填充的值 //这个参数为空时,没有子元素的元素不带key_child属性;还可以为null或者[],同理
"root_id": 0, //根节点的父级ID
"map": { //在节点内映射一些值 //对象的键是节点的新属性; 对象的值是节点的老属性,会赋值给新属性
"value": "ID",
"label": "TypeName",
}
};
You can use npm package array-to-tree https://github.com/alferov/array-to-tree.
It's convert a plain array of nodes (with pointers to parent nodes) to a nested data structure.
Solves a problem with conversion of retrieved from a database sets of data to a nested data structure (i.e. navigation tree).
Usage:
var arrayToTree = require('array-to-tree');
var dataOne = [
{
id: 1,
name: 'Portfolio',
parent_id: undefined
},
{
id: 2,
name: 'Web Development',
parent_id: 1
},
{
id: 3,
name: 'Recent Works',
parent_id: 2
},
{
id: 4,
name: 'About Me',
parent_id: undefined
}
];
arrayToTree(dataOne);
/*
* Output:
*
* Portfolio
* Web Development
* Recent Works
* About Me
*/
You can use this "treeify" package from Github here or NPM.
Installation:
$ npm install --save-dev treeify-js

How to insert elements into specific index in a 2D array in javascript?

I have an object that looks like below
const tableData = [
{
"Location": "London",
"Status": "Unknown"
},
{
"Location": "Delhi",
"Status": "Reachable"
},
{
"Location": "Berlin",
"Status": "Unknown"
},
{
"Location": "Tokyo",
"Status": "Busy"
},
]
Now I want to create a 2D array which will hold this information in a certain way. Here is my code below
const statusOrder = {"Reachable": 0, "Busy": 1, "Unknown": 2}
let statusOrderInfo = Array(Object.keys(statusOrder).length).fill([]);
for(let i=0; i< tableData.length; i++) {
const status = tableData[i]["Status"].trim()
const statusIndex = statusOrder[status]
statusOrderInfo[statusIndex].push(tableData[i])
}
console.log(statusOrderInfo)
As you can see I want each item of the tableData object to be in a certain index of the 2D array. So the item that contains Status as Reachable should be at index 0, the item that contains the Status as Busy should be at index 1 and so on.
So the final output should look like
[
[
{
"Location":"Delhi",
"Status":"Reachable"
}
],
[
{
"Location":"Tokyo",
"Status":"Busy"
}
],
[
{
"Location":"London",
"Status":"Unknown"
},
{
"Location":"Berlin",
"Status":"Unknown"
}
]
]
But I get a wrong output on running the above code even though I am targeting the correct index. What's wrong in my approach?
Using Array#reduce:
const
tableData = [ { "Location": "London", "Status": "Unknown" }, { "Location": "Delhi", "Status": "Reachable" }, { "Location": "Berlin", "Status": "Unknown" }, { "Location": "Tokyo", "Status": "Busy" } ],
statusOrder = {"Reachable": 0, "Busy": 1, "Unknown": 2};
const statusOrderInfo = tableData.reduce((list, e) => {
const index = statusOrder[e.Status];
list[index] = [...(list[index] || []), {...e}];
return list;
}, []);
console.log(statusOrderInfo);
Simple fix on your problem is that just changing your manner to set initial value of statusOrderInfo and use Array.from instead of Array.fill like this:
let statusOrderInfo = Array.from({length: Object.keys(statusOrder).length}, ()=> []);
another solution is set initiali value of statusOrderInfo by empty array, and then in your for loop, after you get the index of current object based on status value, you can check if statusIndex already exist in the statusOrderInfo or not, like this:
const statusOrder = {"Reachable": 0, "Busy": 1, "Unknown": 2}
let statusOrderInfo = [];
for(let i=0; i< tableData.length; i++) {
const status = tableData[i]["Status"].trim()
const statusIndex = statusOrder[status];
if(statusOrderInfo[statusIndex]) statusOrderInfo[statusIndex].push(tableData[i]);
else statusOrderInfo[statusIndex] = [ tableData[i] ]
}
console.log(statusOrderInfo);
another solution, is to use reduce method on array, like this:
const tableData = [{
"Location": "London",
"Status": "Unknown"
},
{
"Location": "Delhi",
"Status": "Reachable"
},
{
"Location": "Berlin",
"Status": "Unknown"
},
{
"Location": "Tokyo",
"Status": "Busy"
},
];
const statusOrder = {"Reachable": 0, "Busy": 1, "Unknown": 2}
const result = tableData.reduce((acc, cur) => {
const index = statusOrder[cur.Status];
if (acc[index]) acc[index].push(cur);
else acc[index] = [cur]
return acc;
}, []);
console.log(result)
Anoter one solution in kind of declartive way:
First of all sort objects by status code using Array#sort
And then just wrap every object to it own array using Array#map
const tableData = [{Location: "London",Status: "Unknown"},{Location: "Delhi",Status: "Reachable"},{Location: "Berlin",Status: "Unknown"},{Location: "Tokyo",Status: "Busy"}]
const statusOrder = {Reachable: 0, Busy: 1, Unknown: 2}
const result = tableData
.sort(({ Status: s1 }, { Status: s2 }) => statusOrder[s1] - statusOrder[s2])
.map((item) => [item]);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
As others explained, reduce would be the best choice for your scenario. Because on every iteration you would create a new Array object.
const tableData = [{ Location: "London", Status: "Unknown" }, { Location: "Delhi", Status: "Reachable" }, { Location: "Berlin", Status: "Unknown" }, { Location: "Tokyo", Status: "Busy" }]
const statusOrder = { "Reachable": 0, "Busy": 1, "Unknown": 2 };
const statusOrderInfo = tableData.reduce((accumulator, currentValue) => {
const index = statusOrder[currentValue.Status];
if (accumulator[index]) {
accumulator[index].push(currentValue);
} else {
accumulator[index] = [currentValue];
}
return accumulator;
}, []);
console.log(statusOrderInfo);
Explanation for what's going on in your code:
As you have used [].fill([]) to fill the Array. It would create only one empty Array object and use it to initialize the actual array. That's why the following snippet behaves the way it should behave :)
const statusOrderInfo = Array(3).fill([]);
statusOrderInfo[0].push(10);
console.log(statusOrderInfo);
/* Results:
[
[ 10 ],
[ 10 ],
[ 10 ]
]
*/
this is my solution:
let statuses = ["Reachable", "Busy", "Unknown"];
const tableData = [
{"Location": "London", "Status": "Unknown"},
{"Location": "Delhi", "Status": "Reachable" },
{"Location": "Berlin", "Status": "Unknown"},
{"Location": "Tokyo", "Status": "Busy"}
];
let result = statuses.map(status => tableData.filter(tableDataItem => tableDataItem.Status == status));
console.log(result);

Populate object key without using mongoose/mongodb

I have this Mongoose Object:
recipe = {
"ingredients": [
{
"_id": "5fc8b729c47c6f38b472078a",
"ingredient": {
"unit": "ml",
"name": "olive oil",
"createdAt": "2020-10-09T10:53:35.567Z",
"updatedAt": "2020-12-03T09:25:43.282Z",
"unitConversion": [
{
"_id": "5fc0a688c1799719d03aea3f",
"value": 20,
"convertedValue": 1,
"unitMeasure": {
"name": "spoon",
"createdAt": "2020-11-19T07:31:11.353Z",
"updatedAt": "2020-11-19T07:31:11.353Z",
"id": "5fb61f3f0a62dc27fc052271"
}
}
],
"id": "5f80412f583af20c8c0aeea1"
},
"quantity": 2,
"unitConv": "5fc0a688c1799719d03aea3f"
}
]
}
What I want to do, populate the unitConv with the object from unitConversion array that has a matching id.
Here is my code:
const newRecipe = recipe.ingredients.map((ingredient) => {
let unit
if (ingredient.unitConv && ingredient.unitConv !== null) {
unit = ingredient.ingredient.unitConversion.reduce((unit) => {
if (unit.id === ingredient.unitConv) {
return unit
}
})
ingredient.unitConv = unit
}
return ingredient
})
It works. ingredient.unitConv gets populated with the object with the matching id from the unitConversion array, but is not an object. It's being added as a string.
This is how ingredient.unitConv is being populated:
"unitConv": "{\n _id: 5fc0a688c1799719d03aea3f,\n value: 20,\n convertedValue: 1,\n unitMeasure: {\n _id: 5fb61f3f0a62dc27fc052271,\n name: 'spoon',\n createdAt: 2020-11-19T07:31:11.353Z,\n updatedAt: 2020-11-19T07:31:11.353Z,\n }\n}"
I tried using JSON.parse() on it, but it won't work, I will get several errors of SyntaxError type (Unexpected token in JSON at position...). I tried JSON.parse(JSON.stringify('the object')), but this won't populate at all.
In your environment it is probably a string, but when passing the code here, it remains as an object. Try to check the data type like this:
recipe = {
ingredients: [
{
_id: '5fc8b729c47c6f38b472078a',
ingredient: {
unit: 'ml',
name: 'olive oil',
createdAt: '2020-10-09T10:53:35.567Z',
updatedAt: '2020-12-03T09:25:43.282Z',
unitConversion: [
{
_id: '5fc0a688c1799719d03aea3f',
value: 20,
convertedValue: 1,
unitMeasure: {
name: 'spoon',
createdAt: '2020-11-19T07:31:11.353Z',
updatedAt: '2020-11-19T07:31:11.353Z',
id: '5fb61f3f0a62dc27fc052271',
},
},
],
id: '5f80412f583af20c8c0aeea1',
},
quantity: 2,
unitConv: '5fc0a688c1799719d03aea3f',
},
],
};
const newRecipe = recipe.ingredients.map((ingredient) => {
let unit;
if (ingredient.unitConv && ingredient.unitConv !== null) {
unit = ingredient.ingredient.unitConversion.reduce((unit) => {
if (unit.id === ingredient.unitConv) {
return unit;
}
});
ingredient.unitConv = unit;
}
return ingredient;
});
console.log(`unitConv is an ${typeof newRecipe[0].unitConv}`);
I would suggest you to user filter function instead of reduce function in your code. In this case you will get a array of objects. Also please check the type of unitConv in your schema.

Map Nested Object Key To Another Key

Working with the following object, how can the roles => name be added to the sites => userPermission object with the matching roleId?
For example, the userPermission key of the first sites entry would be updated to:
"userPermission": {
"roleId": 6,
"roleName": "Field Representative"
}
Once the roleName key has been mapped, there is no longer a need for the roles array and it can be removed from the end result as shown in the expected outcome.
const obj = {
"id": 542,
"createdAt": "2018-12-06T22:34:12.553Z",
"sites": [
{
"id": 10,
"siteId": "sixtysixone",
"edition": "pro",
"userPermission": {
"roleId": 6
}
},
{
"id": 2,
"siteId": "amplify",
"edition": "pro",
"userPermission": {
"roleId": 4
}
}
],
"roles": [
{
"id": 6,
"name": "Field Representative"
},
{
"id": 4,
"name": "Program Manager"
}
]
};
Expected outcome:
const outcome = {
"id": 542,
"createdAt": "2018-12-06T22:34:12.553Z",
"sites": [
{
"id": 10,
"siteId": "sixtysixone",
"edition": "pro",
"userPermission": {
"roleId": 6,
"roleName": "Field Representative"
}
},
{
"id": 2,
"siteId": "amplify",
"edition": "pro",
"userPermission": {
"roleId": 4
"roleName": "Program Manager"
}
}
]
};
I have attempted with a combination of .map and .find, but feel there is a much more simple/readable way to accomplish this.
const outcome = obj.map(o => ({
...o,
sites: o.sites
.map(s => ({
...s,
roleName: o.roles
.find(r => r.id === s.roleId).name,
})),
}));
Create a map of roles:
const roles = new Map(obj.roles.map(({ id, name }) => [id, { roleId: id, roleName: name }]));
Then you can just look up:
const outcome = {
...obj,
sites: obj.sites.map(site => ({
...site,
userPermission: roles.get(site.userPermission.roleId),
}),
roles: undefined,
};
You could loop over the sites and update the userPermission using find
(This assumes that every roleId exists in roles. Otherwise, you need to check if find returns undefined first)
const obj = {"id":542,"createdAt":"2018-12-06T22:34:12.553Z","sites":[{"id":10,"siteId":"sixtysixone","edition":"pro","userPermission":{"roleId":6}},{"id":2,"siteId":"amplify","edition":"pro","userPermission":{"roleId":4}}],"roles":[{"id":6,"name":"Field Representative"},{"id":4,"name":"Program Manager"}]};
obj.sites.forEach(site => {
site.userPermission.roleName =
obj.roles.find(r => r.id === site.userPermission.roleId).name
})
delete obj.roles;
console.log(obj)
You could first turn the roles array into an object to ease the retrieval of the role names:
let rolesMap = obj.roles.reduce((acc, role) =>
(acc[role.id] = role.name, acc),
Object.create(null)
);
Then just loop over the sites array, adding the roleName property to each site userPermission object by fetching the value from rolesMap:
obj.sites.forEach(site => {
if(rolesMap[site.userPermission.roleId]) {
site.userPermission.roleName = rolesMap[site.userPermission.roleId];
}
});
You can skip the if test if you know for sure that each site object will have an associated role object in the roles array. And if you want to create a new sites object then use map instead of forEach.
And finally you can delete the roles property if you want:
delete obj.roles;
Example:
const obj = {"id":542,"createdAt":"2018-12-06T22:34:12.553Z","sites":[{"id":10,"siteId":"sixtysixone","edition":"pro","userPermission":{"roleId":6}},{"id":2,"siteId":"amplify","edition":"pro","userPermission":{"roleId":4}}],"roles":[{"id":6,"name":"Field Representative"},{"id":4,"name":"Program Manager"}]};
let rolesMap = obj.roles.reduce((acc, role) =>
(acc[role.id] = role.name, acc),
Object.create(null)
);
obj.sites.forEach(site => {
if(rolesMap[site.userPermission.roleId]) {
site.userPermission.roleName = rolesMap[site.userPermission.roleId];
}
});
delete obj.roles;
console.log(obj);

Push object keys and its values to array

I have an object like this:
{
"id": 23,
"name": "Jacob",
"link": {
"rel": "self",
"link": "www.abc.com"
},
"company":{
"data":{
"id": 1,
"ref": 324
}
}
I want to store each key with its value to an array in javascript or typescript like this
[["id":23], ["name":"Jacob"], ["link":{......, ......}]] and so on
I am doing this so that I can append an ID for each.
My best guess I would loop through the array and append an ID/a flag for each element, which I don't know how to do as well.... how to address this issue ? thanks
var arr = [];
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
var innerObj = {};
innerObj[prop] = obj[prop];
arr.push(innerObj)
}
}
console.log(arr);
here is demo https://plnkr.co/edit/9PxisCVrhxlurHJYyeIB?p=preview
p.forEach( function (country) {
country.forEach( function (entry) {
entry.push( {"value" : 'Greece', "synonyms" : 'GR'});
});
});
you can try to use experimental Object.entries:
let obj = {
"id": 23,
"name": "Jacob",
"link": {
"rel": "self",
"link": "www.abc.com"
},
"company":{
"data":{
"id": 1,
"ref": 324
}
}};
console.log(Object.entries(obj).map(item => ({[item[0]]:item[1]})));
for unsupported browsers you can use polyfill: https://github.com/es-shims/Object.entries
You could use an iterative/recursive approach with the object and their nested parts. It works for any depths.
function getKeyValue(object) {
return Object.keys(object).reduce(function (result, key) {
return result.concat(
object[key] && typeof object[key] === 'object' ?
getKeyValue(object[key]) :
[[key, object[key]]]
);
}, []);
}
var data = { id: 23, name: "Jacob", link: { rel: "self", link: "www.abc.com" }, company: { data: { id: 1, ref: 324 } } };
console.log(getKeyValue(data));
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use the Object.keys method to get an array of the keys, then use the Array#map method to return a new array containing individual objects for each property.
This ES6 one-liner should do it:
const splitObject = o => Object.keys(o).map(e => ({ [e]: o[e] }));
Or in ES5:
function splitObject(o) {
return Object.keys(o).map(function(e) {
return Object.defineProperty({}, e, {
value: o[e],
enumerable: true
});
});
}
var res = [];
_.transform( {
"id": 23,
"name": "Jacob",
"link": {
"rel": "self",
"link": "www.abc.com"
},
"company": {
"data": {
"id": 1,
"ref": 324
}
}
}, function(result, value, key) {
res.push(key +':'+value);
}, {});
You can use underscore
Supported in all major browser, including IE11
Object.entries() gives you exactly this.
const obj = {
id: 23,
name: 'Jacob',
link: {
rel: 'self',
link: 'www.abc.com'
},
company: {
data: {
id: 1,
ref: 324
}
}
};
Object.entries(obj);
// output:
[
[
"id",
23
],
[
"name",
"Jacob"
],
[
"link",
{
"rel": "self",
"link": "www.abc.com"
}
],
[
"company",
{
"data": {
"id": 1,
"ref": 324
}
}
]
]
var obj=[{"Name":ABC,"Count":123},{"Name":XYZ,"Count":456}];
var arr = [];
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
var innerObj = {};
innerObj[0] = obj[prop];
arr.push(innerObj[0]);
}
}
/* Here above exmple innerobj index set to 0 then we will get same data into arr if u not menstion then arr will conatins arr[0] our result.
then we need to call first record obj arr[0][0] like this*/
const foo = { "bar": "foobar", "foo": "foobar" }
Object.entries(foo)
should result in:
[["bar", "foobar"], ["foo", "foobar"]]
maybe there's a function to pass to convert all commas to colons
Here's the documentation
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries

Categories