How to loop through this nested object? - javascript

I have a dynamically generated JavaScript object which consist of nested objects and arrays. I couldn't find a proper way to list all nested objects, since that particular object is created dynamically.
Here is the object:
var tagdata = {
"Sentence": [{
"NP": [{
"NMP": "cat"
}, {
"NMP": "dog"
}]
}, {
"VP": [{
"KPD": "tree"
}, {
"NP": [{
"NMP": "ball"
}, {
"NMP": "bat"
}]
},{
"NP": [{
"NMP": "ground"
}, {
"NMP": "time"
}]
}]
}]
};
The output I require looks like this:
[{ key: 1, text: 'Sentence' },
{ key: 2, text: 'NP', parent: 1 },
{ key: 3, text: 'VP', parent: 1 },
{ key: 4, text: 'NMP', parent: 2 },
{ key: 5, text: 'NMP', parent: 2 },
{ key: 6, text: 'KPD', parent: 3 },
{ key: 7, text: 'NP', parent: 3 },
{ key: 8, text: 'NP', parent: 3 },
{ key: 9, text: 'cat', parent: 4 },
{ key: 10, text: 'dog', parent: 5 },
{ key: 11, text: 'tree', parent: 6 },
{ key: 12, text: 'NMP', parent: 7 },
{ key: 13, text: 'NMP', parent: 7 },
{ key: 14, text: 'NMP', parent: 8 },
{ key: 15, text: 'NMP', parent: 8 },
{ key: 16, text: 'ball', parent: 12},
{ key: 17, text: 'bat', parent: 13},
{ key: 18, text: 'ground', parent: 14},
{ key: 19, text: 'time', parent: 15},]
This data is to be used in a tree, so the order might be different, but the key:parent relationship should be maintained. Here is the code I've tried with:
let newtags=[{key:1,text:'Sentence'}];
tagdata["Sentence"].map( (elem,x) => {
newtags.push({key:x,text:Object.keys(elem)[0],parent:x});
if(Object.keys(elem)[0].length !== 0){
var y=x+1;
newtags.push({key:y,text:Object.values(elem)[0][x],parent:y});
}
});
console.log(newtags);

Your code works for the first level, but in your attempt to go one level deeper you will assign duplicate key values (y=x+1 when x will have that value in the next iteration of the .map() method as well).
What you need here is a recursive function, one that calls itself to deal with nested levels in the same way as any other level.
You could use this ES6, functional programming solution for that:
function listItems(obj) {
var key = 0;
return (function recurse(obj, parent = undefined) {
return Object(obj) !== obj ? { key: ++key, text: obj, parent }
: Array.isArray(obj) ? Object.keys(obj).reduce( (acc, text) =>
acc.concat(recurse(obj[text], parent)), [])
: Object.keys(obj).reduce( (acc, text) =>
acc.concat({ key: ++key, text, parent },
recurse(obj[text], key)), []);
})(obj);
}
// Sample data
var tagdata = {
"Sentence": [{
"NP": [{ "NMP": "cat" }, { "NMP": "dog" }]
}, {
"VP": [{
"KPD": "tree"
}, {
"NP": [{ "NMP": "ball" }, { "NMP": "bat" }]
},{
"NP": [{ "NMP": "ground" }, { "NMP": "time" }]
}]
}]
};
// Extract the objects and output:
console.log(listItems(tagdata));
.as-console-wrapper { max-height: 100% !important; top: 0; }

I think this does what you want. It uses a recursive algorithm that handles the case where you have an array separately from the case where you have an object. The base case of just a string is handled in processChild.
let state = [];
function processChild(child, parent) {
if (Array.isArray(child)) {
return processArray(child, parent);
}
if (typeof(child) == 'object') {
return processObject(child, parent);
}
let tag = {
key: state.length + 1,
text: child,
};
if (parent) {
tag.parent = parent;
}
state.push(tag);
}
function processObject(object, parent) {
parent = parent || 0;
let keys = Object.keys(object);
for (let i = 0; i < keys.length; i++) {
//console.log(keys[i]);
let tagIndex = state.length + 1;
let text = keys[i];
let tag = {
key: tagIndex,
text: text,
};
if (parent) {
tag.parent = parent;
}
state.push(tag);
let child = object[keys[i]];
processChild(child, tagIndex);
}
}
function processArray(array, parent) {
parent = parent || 0;
for (let i = 0; i < array.length; i++) {
//console.log(array[i]);
let child = array[i];
//console.log('Child', child);
processChild(child, parent);
}
}
function process(){
let initialState = JSON.parse(input.innerHTML);
processChild(initialState);
code.innerHTML = JSON.stringify(state).replace(/},/g,'},\n');
}
#input{
width: 100%;
height: 200px;
}
<textarea id="input">
{
"Sentence": [{
"NP": [{
"NMP": "cat"
}, {
"NMP": "dog"
}]
}, {
"VP": [{
"KPD": "tree"
}, {
"NP": [{
"NMP": "ball"
}, {
"NMP": "bat"
}]
}, {
"NP": [{
"NMP": "ground"
}, {
"NMP": "time"
}]
}]
}]
}
</textarea>
<button onClick="process()">Process</button>
<pre id="code"></pre>

Just for fun, a non-recursive approach.
One array keeps track of the eventual results (results)
One array keeps track of data-to-process
When nested data is found, it is wrapped in a "to-do item" and added to the todo array
Repeat until no items left
var tagData = [{Sentence:[{NP:[{NMP:"cat"},{NMP:"dog"}]},{VP:[{KPD:"tree"},{NP:[{NMP:"ball"},{NMP:"bat"}]},{NP:[{NMP:"ground"},{NMP:"time"}]}]}]}];
var isNode = n => Array.isArray(n);
var isLeaf = n => !isNode(n);
var todoItem = parent => node => ({ parent, node });
var flatten = function(arr) {
var result = [],
todo = arr.map(todoItem(0)),
current, node, parent, key, innerNode;
while (todo.length) {
({node, parent} = todo.pop());
key = result.length + 1;
Object.keys(node).forEach(k => {
innerNode = node[k];
result.push({ key, parent, text: k });
if (isLeaf(innerNode)) {
result.push({
key: key + 1,
parent: key,
text: innerNode
});
} else {
todo = todo.concat(
innerNode.map(todoItem(key)));
}
});
};
return result;
};
// Print output
console.log(
flatten(tagData)
.map(o => [
"key: " + (o.key < 10 ? " " : "") + o.key,
"parent: " + (o.parent < 10 ? " " : "") + o.parent,
o.text
].join(" | "))
);
.as-console-wrapper {
min-height: 100%;
}

Related

Pushing data from this array to another uses the least loop

I want to config data from 'data2' array to 'dataConvert' array
and I want to find another way of optimizing.
let dataConvert = [];
data2 = [
{
time: "2020-7",
tasks: [
{
key: "p1",
value: 15
},
{
key: "p2",
value: 13
},
]
},
{
time: "2020-8",
tasks: [
{
key: "p1",
value: 16
},
{
key: "p2",
value: 19
},
]
},
{
time: "2020-9",
tasks: [
{
key: "p1",
value: 12
},
{
key: "p2",
value: 93
},
]
}
]
After adding data to the 'dataConvert' array, then 'dataConvert' is formatted as follows:
dataConvert = [
["x","2020-7", "2020-8", "2020-9"],
["p1", 15, 16, 12],
["p2", 13, 19, 93]
]
i tried use reduce , I want to find another way of optimizing.
let dateConvert = [], valueConvert = [];
data2.forEach(x=>{
let date = new Date(x.time);
if (date) {
let getYear = date.getFullYear();
let getMonth = date.getMonth() + 1;
let newDate = `${getYear}-${getMonth}-1`;
return dateConvert = [...dateConvert, newDate];
}
})
dateConvert.unshift("x");
// get p1 p2 value
let allTasks = data2.flatMap(x => x.tasks);
valueConvert = Object.values(allTasks.reduce((arr, item) => {
arr[item.key] = arr[item.key] || [item.key];
arr[item.key].push(item.value);
return arr;
}, {}));
dataConvert = [...[dateConvert], ...valueConvert];
thank u.
You could take nested loops and store the index in an object for faster access of key.
const
data = [{ time: "2020-7", tasks: [{ key: "p1", value: 15 }, { key: "p2", value: 13 }] }, { time: "2020-8", tasks: [{ key: "p1", value: 16 }, { key: "p2", value: 19 }] }, { time: "2020-9", tasks: [{ key: "p1", value: 12 }, { key: "p2",value: 93 }] }],
dataConvert = [['x']],
indices = {};
data.forEach(o => {
dataConvert[0].push(o.time);
o.tasks.forEach(({ key, value }) => {
if (!(key in indices)) indices[key] = dataConvert.push([key]) - 1;
dataConvert[indices[key]].push(value);
});
});
console.log(dataConvert);
.as-console-wrapper { max-height: 100% !important; top: 0; }

How to convert json to tree array in JS?

I would like to convert this json / object to this specific structure below to allow me to use a treeList component.
I've tried to build a recursive function but I didn't find the solution yet.
Thanks for your help
const data = {
parent1: {
child1: { bar: "1" },
child2: "2"
},
parent2: {
child1: "1"
}
}
to
const treeData = [
{
title: "parent1",
key: "parent1",
children: [
{
title: "child1",
key: "child1",
children: [{ title: "bar", key: "bar", value: "1" }]
},
{
title: "child2",
key: "child2",
value: "2"
}
],
},
{
title: "parent2",
key: "parent2",
children: [
{
title: "child1",
key: "child1",
value: "1"
}
]
}
]
You could take an iterative and recursive approach.
function getNodes(object) {
return Object
.entries(object)
.map(([key, value]) => value && typeof value === 'object'
? { title: key, key, children: getNodes(value) }
: { title: key, key, value }
);
}
const data = { parent1: { child1: { bar: "1" }, child2: "2" }, parent2: { child1: "1" } },
result = getNodes(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
just share sample, a little different from yours. But it give you a hint with recursive function.
https://jsfiddle.net/segansoft/7bdxmys4/1/
function getNestedChildren(arr, parent) {
var out = []
for (var i in arr) {
if (arr[i].parent == parent) {
var children = getNestedChildren(arr, arr[i].id)
if (children.length) {
arr[i].children = children
}
out.push(arr[i])
}
}
return out
}
var flat = [{
id: 1,
title: 'hello',
parent: 0
},
{
id: 2,
title: 'hello',
parent: 0
},
{
id: 3,
title: 'hello',
parent: 1
},
{
id: 4,
title: 'hello',
parent: 3
},
{
id: 5,
title: 'hello',
parent: 4
},
{
id: 6,
title: 'hello',
parent: 4
},
{
id: 7,
title: 'hello',
parent: 3
},
{
id: 8,
title: 'hello',
parent: 2
}
]
var nested = getNestedChildren(flat, 0)
document.write('<pre>' + JSON.stringify(nested, 0, 4) + '</pre>');

javascript array tree search keep the node and parents

Trying to implement a tree search function which takes an array(tree structure) and a string keyword, would return an tree array but only keep the matched nodes and its parents.
function search(nodes, keyword){
}
const nodes = [
{
value: "1-1",
children: [
{ value: "1-1-1"},
{ value: "1-1-2", children:[
{
value: "1-1-2-1",
children: [
{ value: "1-1-2-1-1" },
{ value: "1-1-2-1-2" }
]
},
{
value: "1-1-2-2"
}
] }
]
},
{
value: "1-2",
children: [
{ value: "1-2-1"},
{ value: "1-2-2", children:[
{
value: "1-2-2-1",
children: [
{ value: "1-2-2-1-1" },
{ value: "1-2-2-1-2" }
]
},
{
value: "1-2-2-2"
}
] }
]
},
];
expected output would be an tree with nodes' values contain "1-1-2-1" and its parents as below
const searchedNodes = search(nodes, "1-1-2-1");
[
{
value: "1-1",
children: [
{ value: "1-1-2", children:[
{
value: "1-1-2-1",
children: [
{ value: "1-1-2-1-1" }
]
}
] }
]
}
]
*/
2018-06-26 Updated
I made a working one(DFS) but probably not pretty efficient.
const search = (nodes, keyword) => {
let newNodes = [];
for (let n of nodes) {
if (n.children) {
const nextNodes = this.keywordFilter(n.children, keyword);
if (nextNodes.length > 0) {
n.children = nextNodes;
} else if (n.label.toLowerCase().includes(keyword.toLowerCase())) {
n.children = nextNodes.length > 0 ? nextNodes : [];
}
if (
nextNodes.length > 0 ||
n.label.toLowerCase().includes(keyword.toLowerCase())
) {
newNodes.push(n);
}
} else {
if (n.label.toLowerCase().includes(keyword.toLowerCase())) {
newNodes.push(n);
}
}
}
return newNodes;
};
You need to iterate the nodes of the same level and check if the value is equal, then take that node and exit the loop. Otherwise check the children and generate a new object for preventing to mutate the original data.
function search(nodes, value) {
var result;
nodes.some(o => {
var children;
if (o.value === value) {
return result = o;
}
if (o.children && (children = search(o.children, value))) {
return result = Object.assign({}, o, { children });
}
});
return result && [result];
}
const nodes = [{ value: "1-1", children: [{ value: "1-1-1" }, { value: "1-1-2", children: [{ value: "1-1-2-1", children: [{ value: "1-1-2-1-1" }, { value: "1-1-2-1-2" }] }, { value: "1-1-2-2" }] }] }, { value: "1-2", children: [{ value: "1-2-1" }, { value: "1-2-2", children: [{ value: "1-2-2-1", children: [{ value: "1-2-2-1-1" }, { value: "1-2-2-1-2" }] }, { value: "1-2-2-2" }] }] }];
console.log(search(nodes, "1-1-2-1"));
.as-console-wrapper { max-height: 100% !important; top: 0; }

deleting an element in nested array using filter() function

I have been trying to delete an element with an ID in nested array.
I am not sure how to use filter() with nested arrays.
I want to delete the {id: 111,name: "A"} object only.
Here is my code:
var array = [{
id: 1,
list: [{
id: 123,
name: "Dartanan"
}, {
id: 456,
name: "Athos"
}, {
id: 789,
name: "Porthos"
}]
}, {
id: 2,
list: [{
id: 111,
name: "A"
}, {
id: 222,
name: "B"
}]
}]
var temp = array
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array[i].list.length; j++) {
temp = temp.filter(function(item) {
return item.list[j].id !== 123
})
}
}
array = temp
You can use the function forEach and execute the function filter for every array list.
var array = [{ id: 1, list: [{ id: 123, name: "Dartanan" }, { id: 456, name: "Athos" }, { id: 789, name: "Porthos" }] }, { id: 2, list: [{ id: 111, name: "A" }, { id: 222, name: "B" }] }];
array.forEach(o => (o.list = o.list.filter(l => l.id != 111)));
console.log(array);
.as-console-wrapper { max-height: 100% !important; top: 0; }
To remain the data immutable, use the function map:
var array = [{ id: 1, list: [{ id: 123, name: "Dartanan" }, { id: 456, name: "Athos" }, { id: 789, name: "Porthos" }] }, { id: 2, list: [{ id: 111, name: "A" }, { id: 222, name: "B" }] }],
result = array.map(o => ({...o, list: o.list.filter(l => l.id != 111)}));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You could create a new array which contains elements with filtered list property.
const result = array.map(element => (
{
...element,
list: element.list.filter(l => l.id !== 111)
}
));
You can use Object.assign if the runtime you are running this code on does not support spread operator.
Array.filter acts on elements:
var myArray = [{something: 1, list: [1,2,3]}, {something: 2, list: [3,4,5]}]
var filtered = myArray.filter(function(element) {
return element.something === 1;
// true = keep element, false = discard it
})
console.log(filtered); // logs [{something: 1, list: [1,2,3]}]
You can use it like this:
var array = [{
id: 1,
list: [{
id: 123,
name: "Dartanan"
}, {
id: 456,
name: "Athos"
}, {
id: 789,
name: "Porthos"
}]
}, {
id: 2,
list: [{
id: 111,
name: "A"
}, {
id: 222,
name: "B"
}]
}]
for (var i = 0; i < array.length; ++i) {
var element = array[i]
// Filter the list
element.list = element.list.filter(function(listItem) {
return listItem.id !== 111 && listItem.name !== 'A';
})
}
console.log(array)

how to flat array js

How to flatten this array :
[
{ID: 0 , TITLE: 'A', children: [{ID: 1, TITLE: 'AA'}]},
{ID: 2 , TITLE: 'B', children: []},
{ID: 3 , TITLE: 'C', children: [{ID: 4, TITLE: 'CC', children:[{ID: 5, TITLE: 'CCC'}]}]}
]
To get something like this :
A
A / AA
B
C / CC / CCC
You could use store the nested item of the actual object and take an iterative and recursive approach with a closure over the path.
For an incrementing ID, you could use either an additional variable for incrementing if a new row is found or iterate at the end the given array and add the index as id.
This proposal uses an additional id variable, because it requires no extra loop.
var array = [{ ID: 0, TITLE: 'A', children: [{ ID: 1, TITLE: 'AA' }] }, { ID: 2, TITLE: 'B', children: [] }, { ID: 3, TITLE: 'C', children: [{ ID: 4, TITLE: 'CC', children: [{ ID: 5, TITLE: 'CCC' }] }] }],
id = 0,
result = array.reduce(function f(p) {
return function (r, o) {
var temp = p.concat(o.TITLE);
r.push({ ID: id++, TITLE: temp.join('/') });
if (o.children) {
o.children.reduce(f(temp), r);
}
return r;
};
}([]), []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
There's no need to complicate things here, you just need a function that loop over the array items, get their TITLE and if the item has children, we call the function recursively:
function getLevels(array, parent) {
var results = [];
array.forEach(function(el) {
results.push(!parent ? el["TITLE"] : parent + "/" + el["TITLE"]);
let prefix = parent ? parent + "/" + el["TITLE"] : el["TITLE"];
if (el.children && el.children.length > 0) {
getLevels(el.children, prefix).forEach(function(child) {
results.push(child);
});
}
});
return results;
}
Demo:
var arr = [{
ID: 0,
TITLE: 'A',
children: [{
ID: 1,
TITLE: 'AA'
}]
},
{
ID: 2,
TITLE: 'B',
children: []
},
{
ID: 3,
TITLE: 'C',
children: [{
ID: 4,
TITLE: 'CC',
children: [{
ID: 5,
TITLE: 'CCC'
}]
}]
}
];
function getLevels(array, parent) {
var results = [];
array.forEach(function(el) {
results.push(!parent ? el["TITLE"] : parent + "/" + el["TITLE"]);
let prefix = parent ? parent + "/" + el["TITLE"] : el["TITLE"];
if (el.children && el.children.length > 0) {
getLevels(el.children, prefix).forEach(function(child) {
results.push(child);
});
}
});
return results;
}
console.log(getLevels(arr));
Edit:
This is how to proceed to get the id in the array:
results.push({
"ID": el["ID"],
"TITLE": (!parent ? el["TITLE"] : parent + "/" + el["TITLE"])
});
Demo:
var arr = [{
ID: 0,
TITLE: 'A',
children: [{
ID: 1,
TITLE: 'AA'
}]
},
{
ID: 2,
TITLE: 'B',
children: []
},
{
ID: 3,
TITLE: 'C',
children: [{
ID: 4,
TITLE: 'CC',
children: [{
ID: 5,
TITLE: 'CCC'
}]
}]
}
];
function getLevels(array, parent) {
var results = [];
array.forEach(function(el) {
results.push({
"ID": el["ID"],
"TITLE": (!parent ? el["TITLE"] : parent + "/" + el["TITLE"])
});
let prefix = parent ? parent + "/" + el["TITLE"] : el["TITLE"];
if (el.children && el.children.length > 0) {
getLevels(el.children, prefix).forEach(function(child) {
results.push(child);
});
}
});
return results;
}
console.log(getLevels(arr));
Did you want something like this?
function merge(input, flat_array) {
if (!flat_array) {
// this is the initial recursive call, make the input
// (an array) have the same { child:[] } structure
// as children calls will end up having
flat_array = [];
input = { children: input };
} else {
// since the parent call is just a plain array it doesnt
// have a title so dont bother inserting anything
flat_array.push(input.TITLE);
}
for (let i in input.children) {
// recursively merge the children in into the flat array
// too
merge(input.children[i], flat_array);
}
return flat_array;
}
merge(x);
produces:
["A", "AA", "B", "C", "CC", "CCC"]

Categories

Resources