restructure json based on parent - javascript

I am studying the use of reduce in javascript, and I am trying to restructure an Array of Objects in a generic way - need to be dynamic.
flowchart - i get totaly lost
I started with this through.
Every ID becomes a Key.
Every PARENT identifies which Key it belongs to.
i have this:
const in = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
]
i want this
out = {
"Futebol": {
"Ball": {
"Nike": {}
}
},
"Volley": {}
}
i try it - and i had miserably failed.
const tree = require('./mock10.json')
// Every ID becomes a Key.
// Every PARENT identifies which Key it belongs to.
const parsedTree = {}
tree.reduce((acc, item) => {
if (parsedTree.hasOwnProperty(item.parent)){
if (parsedTree[`${item.parent}`].length > 0) {
parsedTree[`${item.parent}`][`${item.id}`] = {}
} else {
parsedTree[`${item.parent}`] = { [`${item.id}`]: {} }
}
} else {
// i get lost in logic
}
}, parsedTree)
console.log(parsedTree)

Got a working code for you, feel free to ask me about the implementation
Hope it helps :)
const arrSample = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
]
const buildTree = (arr) => {
return arr.reduce(([tree, treeMap], { id, parent }) => {
const val = {}
treeMap.set(id, val)
if (!parent) {
tree[id] = val
return [tree, treeMap]
}
if (!treeMap.has(parent)) {
const parentVal = { [id]: val }
treeMap.set(parent, parentVal)
tree[parent] = parentVal
return [tree, treeMap]
}
const newParentValue = treeMap.get(parent)
newParentValue[id] = val
treeMap.set(parent, newParentValue)
return [tree, treeMap]
}, [{}, new Map()])
}
const [result] = buildTree(arrSample)
console.log(JSON.stringify(result, 0, 2))

You could use reduce method for this and store each id on the first level of the object. This solution will work if the objects in the array are in the correct order as in the tree structure.
const data = [{"id":"Futebol","parent":null},{"id":"Ball","parent":"Futebol"},{"id":"Nike","parent":"Ball"},{"id":"Volley","parent":null}]
const result = data.reduce((r, { id, parent }) => {
if (!parent) {
r[id] = {}
r.tree[id] = r[id]
} else if (r[parent]) {
r[parent][id] = {}
r[id] = r[parent][id]
}
return r
}, {tree: {}}).tree
console.log(result)

If reduce solution is just an option, you can try this way:
var input = [
{
"id": "Ball",
"parent": "Futebol"
},
{
"id": "Nike",
"parent": "Ball"
},
{
"id": "Volley",
"parent": null
}
];
var output = {};
input.forEach(item => {
var temp = input.find(x => x.id === item.parent);
if (temp) {
temp[item.id] = {};
}
});
input = input.filter(item => !input.find(x => x.hasOwnProperty(item.id)));
input.forEach(item => {
if (!item.parent) {
output[item.id] = {};
} else {
for (var [id, value] of Object.entries(item)) {
if (typeof value === 'object') {
output[item.parent] = { [item.id]: { id: {} } };
}
}
}
})
console.log(output);

I have tried many things, but none works if we use an Array.prototype.reduce
As there are missing parents, and the elements are out of order, plus the fact that there can be an infinity of levels, I really do not believe that this question can be resolved with a simple reduce
This code should work whatever the cases :
- if all parents are not declared
- if there are infinitely many levels
- if they are in disorder
const origin =
[ { id: 'Ball', parent: 'Futebol' }
, { id: 'Nike', parent: 'Ball' }
, { id: 'Volley', parent: null }
, { id: 'lastOne', parent: 'level4' } // added
, { id: 'level4', parent: 'Nike' } // added
, { id: 'bis', parent: 'Nike' } // added
];
const Result = {} // guess who ?
, Parents = [] // tempory array to keep parents elements address by key names
;
let nbTodo = origin.length // need this one to verify number of elements to track
;
// set all the first levels, add a todo flags
origin.forEach(({id,parent},i,ori)=>
{
ori[i].todo = true // adding todo flag
if (parent===null)
{
Result[id] = {} // new first level element
ori[i].todo = false // one less :)
nbTodo--
Parents.push(({ref:id,path:Result[id]}) ) // I know who you are!
}
else if (origin.filter(el=>el.id===parent).length===0) // if he has no parent...
{
Result[parent] = {} // we create it one
Parents.push({ref:parent,path:Result[parent]} )
}
})
// to put the children back in their parents' arms
while(nbTodo>0) // while there are still some
{
origin.forEach(({id,parent,todo},i,ori)=> // little by little we find them all
{
if(todo) // got one !
{
let pos = Parents.find(p=>p.ref===parent) // have parent already been placed?
if(pos)
{
ori[i].todo = false // to be sure not to repeat yourself unnecessarily
nbTodo-- // one less :)
pos.path[id] = {} // and voila, parentage is done
Parents.push(({ref:id,path:pos.path[id]}) ) // he can now take on the role of parent
}
}
})
}
for (let i=origin.length;i--;) { delete origin[i].todo } // remove todo flags
console.log( JSON.stringify(Result, 0, 2) )
.as-console-wrapper { max-height: 100% !important; top: 0; }
I finaly made this one, based on this previous on, and done with a first step by a reduce...
to by pass the Array of Parents, I made a recursive function for searching each parent elements thru the levels of parsedTree result.
here is the code:
const Tree =
[ { id: 'Ball', parent: 'Futebol' }
, { id: 'Nike', parent: 'Ball' }
, { id: 'Volley', parent: null }
, { id: 'lastOne', parent: 'level4' } // added
, { id: 'level4', parent: 'Nike' } // added
, { id: 'bis', parent: 'Nike' } // added
];
const parsedTree = Tree.reduce((parTree, {id,parent},i ) => {
Tree[i].todo = false
if (parent===null)
{ parTree[id] = {} }
else if (Tree.filter(el=>el.id===parent).length===0) // if he has no parent...
{ parTree[parent] = { [id]: {} } }
else
{ Tree[i].todo = true }
return parTree
}, {})
function parsedTreeSearch(id, part) {
let rep = null
for(let kId in part) {
if (kId===id)
{ rep = part[kId] }
else if (Object.keys(part[kId]).length)
{ rep = parsedTreeSearch(id, part[kId]) }
if (rep) break
}
return rep
}
while (Boolean(Tree.find(t=>t.todo))) {
Tree.forEach(({id,parent,todo},i)=>{ // little by little we find them all
if (todo) {
let Pelm = parsedTreeSearch(parent, parsedTree)
if (Boolean(Pelm)) {
Pelm[id] = {}
Tree[i].todo = false
} } }) }
for (let i=Tree.length;i--;) { delete Tree[i].todo } // remove todo flags
console.log( JSON.stringify( parsedTree ,0,2))
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

Convert file path into object

Say I have the following strings:
"files/photos/foo.png"
"files/videos/movie.mov"
and I want to convert them to the following object:
{
name: "files"
children: [{
name: "photos",
children: [{
name: "foo.png",
id: "files/photos/foo.png"
}]
},{
name: "videos",
children: [{
name: "movie.mov",
id: "files/videos/movie.mov"
}]
}]
}
What would be the best approach for doing so? I've tried writing some recursive functions, however admit that I'm struggling at the moment.
Here's a quick snippet with a possible solution. It uses nested loops, the outer splitting each path by the delimeter and pop()ing the file portion out of the array. The inner iterates the parts of the path and constructs the heirarchy by reasigning branch on each iteration. Finally the file portion of the path is added to the deepest branch.
const data = [
'files/photos/foo.png',
'files/photos/bar.png',
'files/videos/movie.mov',
'docs/photos/sd.jpg'
];
const tree = { root: {} }
for (const path of data) {
const parts = path.split('/');
const file = parts.pop();
let branch = tree, partPath = '';
for (const part of parts) {
partPath += `${part}/`;
if (partPath === `${part}/`) {
tree.root[partPath] = (tree[partPath] ??= { name: part, children: [] });
} else if (tree[partPath] === undefined) {
tree[partPath] = { name: part, children: [] };
branch.children.push(tree[partPath]);
}
branch = tree[partPath];
}
branch.children.push({ name: file, id: path });
}
const result = Object.values(tree.root)
console.log(JSON.stringify(result, null, 2))
.as-console-wrapper { max-height: 100% !important; top: 0; }
.as-console-row::after { display: none !important; }
Or as a function.
function mergeAssets(assets) {
const tree = { root: {} }
for (const path of data) {
const parts = path.split('/');
const file = parts.pop();
let branch = tree, partPath = '';
for (const part of parts) {
partPath += `${part}/`;
if (partPath === `${part}/`) {
tree.root[partPath] = (tree[partPath] ??= { name: part, children: [] });
} else if (tree[partPath] === undefined) {
tree[partPath] = { name: part, children: [] };
branch.children.push(tree[partPath]);
}
branch = tree[partPath];
}
branch.children.push({ name: file, id: path });
}
return {
name: "assets",
children: Object.values(tree.root)
}
}
const data = [
'files/photos/foo.png',
'files/photos/bar.png',
'files/videos/movie.mov',
'docs/photos/sd.jpg'
];
const result = mergeAssets(data);
console.log(JSON.stringify(result, null, 2))
I was able to find a solution using a recursive function. If others have any tips on how to improve this, I'd love to hear.
function mergeObjects(parentArray,path,originalName){
if(originalName === undefined){
originalName = path;
}
const parts = path.split("/");
var nextPart = "";
parts.forEach((part, index) => index > 0 ? nextPart += (nextPart !== "" ? "/" : "") + part : null);
//does the parentArray contain a child with our name?
const indexOfChild = parentArray.findIndex(child => child.name === parts[0]);
if(indexOfChild === -1){
//this item does not exist
if(parts.length > 1){
var index = parentArray.push({
name: parts[0],
children : []
}) - 1;
mergeObjects(parentArray[index].children,nextPart,originalName);
}else{
parentArray.push({
name: parts[0],
id : originalName
});
}
}else{
//this item already exists
if(parts.length > 1){
mergeObjects(parentArray[indexOfChild].children,nextPart,originalName);
}
}
}
And the function is called with the following:
function mergeAssets(assets){
var obj = {
name: "assets",
children: []
};
assets.forEach(asset => mergeObjects(obj.children,asset));
return obj;
}

Optimize solution for faster execution - NodeJs

I am trying optimize a solution which is used to group the objects based on value present in another array (mapping ) which is written is NodeJS.
function workerFunction(report) {
const groupedArray = []
const granularityKey = 'Month'
const mapping = ['TestField1', 'TestField2'] //will be dynamic based on json input
let activeIIndex = 0;
while (activeIIndex < report.length) {
const index = groupedArray
.findIndex(item => mapping
.every((column) => item[column] === report[activeIIndex][column]))
if (index === -1) {
report[activeIIndex][report[activeIIndex][granularityKey]] = report[activeIIndex]['Cost']
groupedArray.push(report[activeIIndex])
} else {
groupedArray[index][report[activeIIndex][granularityKey]] = report[activeIIndex]['Cost']
}
activeIIndex++;
}
return groupedArray
}
The input to the workerFunction(report) will look like this (usually the array size will be above 300k),
[
{
"TestField1":"value",
"TestField2":"value2",
"Cost":12.5555,
"Month":10
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":142.5555,
"Month":10
},
{
"TestField1":"value6",
"TestField2":"value4",
"Cost":15.87,
"Month":10
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":16.5555,
"Month":11
}
]
The expected output after passing this json object through workerFunction will be
[
{
"TestField1":"value",
"TestField2":"value2",
"Cost":12.5555,
"Month":10,
"10":12.5555
},
{
"TestField1":"value3",
"TestField2":"value4",
"Cost":142.5555,
"Month":10,
"10":142.5555,
"11":16.5555
},
{
"TestField1":"value6",
"TestField2":"value4",
"Cost":15.87,
"Month":10,
"10":15.87
},
]

how to populate tree from flatTreeNode?

I have treeFlatNode array i want to structure it in tree format. or can i display this array in tree directly in angular.
data=[
{
expandable: true
level: 0
name: "2021-12-31"
path: null
},
{
expandable: false
level: 2
name: "A.txt"
path: "2021-12-31/B/C/A.txt"
}
]
required format
tree=[
name:"2021-12-03",
children:[
name:"B",
children:[{
name:"C"
children:[{
name:"A.txt"
children:[]
}]
}]
]
]
You could use an object (map) that maps a (sub)path to a node in the final tree. If it doesn't exist yet, it is added to the parent's children.
As your tree structure actually represents a forest (there can be multiple roots), I would name the result variable forest instead of tree
Snippet:
function toForest(data) {
const roots = [];
const map = {};
for (const obj of data) {
let key = "";
let children = roots;
for (const name of (obj.path ?? obj.name).split("/")) {
let child = map[key += "/" + name];
if (!child) children.push(map[key] = child = { name, children: [] });
({children} = child);
}
}
return roots;
}
// Example run
let data = [{expandable: true,level: 0,name: "2021-12-31",path: null}, {expandable: false,level: 2,name: "A.txt",path: "2021-12-31/B/C/A.txt"}];
let forest = toForest(data);
console.log(forest);
So, to transform your data structure to the desired one, you can use following function (with comments =) ):
transform(data){
const tree = [];
for (let node of data) {
// If there's no path it's a parent node
// but add it only if it doesn't exist yet
if (node.path === null && tree.every(n => n.name !== node.name)) {
tree.push({ name: node.name, children: [] });
continue;
}
// Extract name of parent node and other nodes
const [parentNodeName, ...pathElems]: string[] = node.path.split('/');
// Look-up for the parent node
let parentNode = tree.find(t => t.name === parentNodeName);
// If parent doesn't exist yet, so we create it here
if (!parentNode) {
parentNode = { name: parentNodeName, children: [] }
}
let children = parentNode.children;
// If the level of the node is relevant
// otherwise simply iterate over all pathElems
for(let i = 0; i <= node.level; i ++) {
let child = children.find(c => c.name === pathElems[i]);
// If the child doesn't exist yet - create it
if (!child) {
child = {
name: pathElems[i],
children: []
}
children.push(child);
children = child.children;
continue;
}
// Child does exist, so use it's children for the next iteration
children = child.children;
}
}
return tree;
}
And you can call this function, for example, in ngOnInit:
ngOnInit() {
this.tree = this.transform(this.data);
}
I do not use Angular, but if you just need to convert your flat to nested:
var data = [
{
expandable: true,
level: 0,
name: "2021-12-31",
path: null
},
{
expandable: false,
level: 2,
name: "A.txt",
path: "2021-12-31/B/C/A.txt"
}
]
var to_nested = function(flat) {
var nested = []
var cache = {}
var l = flat.length
var cache_assert = function(name) {
if (cache[name] == null) {
cache[name] = {
name: name,
children: []
}
}
}
for (var i = 0; i < l; i++) {
var current_node = flat[i]
cache_assert(current_node.name)
if (current_node.path == null) {
nested.push(cache[current_node.name])
} else {
var names = current_node.path.split("/")
var parent_name = names.shift()
cache_assert(parent_name)
names.forEach(function(name) {
cache_assert(name)
cache[parent_name].children.push(cache[name])
parent_name = name
})
}
}
return nested
}
var a = to_nested(data)
console.log('a: ', a)
console.log('a: ', a[0].children)
console.log('a: ', a[0].children[0].children)
And if you want to return to flat:
var level = 0
var cache = []; cache[level] = a.slice(0)
var parent = []; parent[level] = null
var index = []; index[level] = 0
while (level >= 0) {
var node = cache[level][index[level]]
if (node != null) {
console.log('node: ', node)
if (
node['children'] != null &&
Object.prototype.toString.call(node['children']) === '[object Array]' &&
node['children'].length
) {
level++
index[level] = 0
parent[level] = Object.assign({}, node)
delete parent[level]['children']
cache[level] = node['children'].slice(0)
} else {
index[level]++
}
} else {
parent[level] = null
level--
index[level]++
}
}

How to append the object into existing json array of objects

I am having json object like below which will be dynamic,
let data_existing= [
{
"client":[
{
"name":"aaaa",
"filter":{
"name":"123456"
}
}
]
},
{
"server":[
{
"name":"qqqqq",
"filter":{
"name":"984567"
}
}
]
},
]
From the inputs i will get an object like below,
let data_new = {
"client":[
{
"name":"bbbbb",
"filter":{
"name":"456789"
}
}
]
}
I need to append this object into the existing "client" json object. Expected output will be like,
[
{
"client":[
{
"name":"aaaa",
"filter":{
"name":"123456"
}
},
{
"name":"bbbb",
"filter":{
"name":"456789"
}
}
]
},
{
"server":[
{
"name":"qqqqq",
"filter":{
"name":"984567"
}
}
]
}
]
And, if the "data_new" is not exists in the main objects, it should as new objects like below, for example,
let data_new = {
"server2":[
{
"name":"kkkkk",
"filter":{
"name":"111111"
}
}
]
}
output will be like,
[
{
"client":[
{
"name":"aaaa",
"filter":{
"name":"123456"
}
},
]
},
{
"server":[
{
"name":"qqqqq",
"filter":{
"name":"984567"
}
}
]
},
{
"server2":[
{
"name":"kkkkk",
"filter":{
"name":"11111"
}
}
]
}
]
I tried the below method, but it is not working as expected. Some help would be appreciated.
Tried like below and not worked as expected,
function addData(oldData, newData) {
let [key, value] = Object.entries(newData)[0]
return oldData.reduce((op, inp) => {
if (inp.hasOwnProperty(key)) {
console.log("111");
op[key] = inp[key].concat(newData[key]);
} else {
console.log(JSON.stringify(inp));
op = Object.assign(op, inp);
}
return op
}, {})
}
Your function seems to work when the key already belongs to data_existing (e.g.: "client").
But you have to handle the second use-case: when the key was not found in the objects of data_existing (e.g.: "server2").
This shall be performed after the reduce loop, adding the new item to data_existing if the key was not found.
Here is an example of how you could achieve that:
function addData(inputData, inputItem) {
const [newKey, newValue] = Object.entries(inputItem)[0];
let wasFound = false; // true iif the key was found in list
const res = inputData.reduce((accumulator, item) => {
const [key, value] = Object.entries(item)[0];
const keyMatch = key === newKey;
if (keyMatch) {
wasFound = true;
}
// concatenate the lists in case of key matching
const newItem = { [key]: keyMatch ? [...value, ...newValue] : value };
return [...accumulator, newItem];
}, []);
if (!wasFound) {
res.push(inputItem); // if key was not found, add item to the list
}
return res;
}
Hope it helps.

How to pick data from array and create a single Object from it?

I want to create a single object from an array of objects. Please refer the code provided.
Here's the input array
let queryArr = [
{
query: {
filter: {
term: {
search: 'complete',
}
}
}
},
{
query: {
notFilter: {
term: {
search: 'failed',
}
}
}
},
{
query: {
bool: {
term: {
search: 'complete',
}
}
}
}
]
The expected output
let oneQuery = {query: {
bool: { ... },
filter: { ... },
notFilter: { ... } // data from respective array object key
}};
The function I wrote
function createQuery(arr){
for(let i = 0; i < arr.length; i++){
if(Object.keys(arr[i].query === 'bool')){
oneQuery.query.bool = arr[i].query.bool;
}
if(Object.keys(arr[i].query === 'filter')){
oneQuery.query.filter = arr[i].query.filter;
}
if(Object.keys(arr[i].query === 'notFilter')){
oneQuery.query.notFilter = arr[i].query.notFilter;
}
}
return oneQuery;
}
createQuery(queryArr);
The output I'm getting:
query: {
bool: { ... },
filter: undefined,
notFilter: undefined
}
I don't get what I'm doing wrong here. A solution using reduce or map will be preferred.
Use Array.map() to get an array with the contents of each query property, then spread into Object.assign() to combine to a single object:
const queryArr = [{"query":{"filter":{"term":{"search":"complete"}}}},{"query":{"notFilter":{"term":{"search":"failed"}}}},{"query":{"bool":{"term":{"search":"complete"}}}}];
const createQuery = (arr) => ({
query: Object.assign({}, ...queryArr.map(({ query }) => query))
});
console.log(createQuery(queryArr));
To fix your code, initialize the query item, and get the 1st key from each item in the array - arr[i].query)[0]:
const queryArr = [{"query":{"filter":{"term":{"search":"complete"}}}},{"query":{"notFilter":{"term":{"search":"failed"}}}},{"query":{"bool":{"term":{"search":"complete"}}}}]
function createQuery(arr){
const oneQuery = { query: {} };
for(let i = 0; i < arr.length; i++){
if(Object.keys(arr[i].query)[0] === 'bool'){
oneQuery.query.bool = arr[i].query.bool;
}
if(Object.keys(arr[i].query)[0] === 'filter'){
oneQuery.query.filter = arr[i].query.filter;
}
if(Object.keys(arr[i].query)[0] === 'notFilter'){
oneQuery.query.notFilter = arr[i].query.notFilter;
}
}
return oneQuery;
}
console.log(createQuery(queryArr));
You problem seems to be this line
Object.keys(arr[i].query === 'filter')
This evaluates to Object.keys(true) or Object.keys(false)
Use reduce
queryArr.reduce( (acc, c) => (
acc[ Object.keys(c.query)[0] ] = Object.values(c.query)[0], //set the first key and value to accumulator
acc ), //return the accumulator
{}); //initialize accumulator to {}

Categories