Given the following data structure:
[
{
"name":"root",
"children":[
{
"name":"de",
"children":[
{
"name":"de",
"children":[
{
"name":"project-1",
"children":[
]
},
{
"name":"project-2",
"children":[
]
}
]
}
]
}
]
}
]
Expected:
[
{
"name":"project-1",
"children":[
]
},
{
"name":"project-2",
"children":[
]
}
]
I want to remove a level if there is only one child. In this example I want to have a new array that only contains the children of the "root" level without root itself.
I would do that with reduce but still cant wrap my head around reduce in combination with recursion. Any ideas?
You can simply use map and flatten arrays afterwards.
.map(o => o.children).flat()
EDIT: updated answer after figuring out the real question
Still you can use map and flatten logic but in a recursive manner.
function removeSingleChildElms (o) {
if (!o.children) return
if (o.children.length === 1) {
return o.children.map(removeSingleChildElms).flat()
} else {
return o.children
}
}
EDIT2:
Some explanation: The problem is transforming array of object(s) into array of different objects. I don't choose reduce, because the problem doesn't care about relationship/logic among sibling elements. It's just about transforming, hence map will just work good enough.
The problem asks to 'skip' objects with 1 child. This is recurring part, meaning: If you see an object satisfying this condition you go deeper for mapping. In any other valid condition, children stay same (else case)
Tree transformation can be made easy by breaking the task down into two parts:
a function for transforming a single node
a function for transforming an array of nodes
To transform a single node, we write transform1
if there are no children, we have found a leaf node, return the singleton node
if there is just one child, drop the node and return the transformation of its only child
otherwise, the node has multiple children, call our second function transformAll
const transform1 = ({ children = [], ...node }) =>
children.length === 0 // leaf
? [ node ]
: children.length === 1 // singleton
? transform1 (...children)
: transformAll (children) // default
To transform an array of nodes, we write transformAll -
const transformAll = (arr = []) =>
arr .flatMap (transform1)
As you can see, transformAll calls transform1, which also calls transformAll. This technique is called mutual recursion and it's a great way to process recursive data structures like the one proposed in your question.
To ensure our function works properly, I've modified the tree to contain more data scenarios. Note, our program works for any nodes with a children property. All other properties are displayed in the result -
const data =
[ { name: "a"
, children:
[ { name: "a.a"
, children:
[ { name: "a.a.a"
, children: []
}
, { name: "a.a.b"
, foo: 123
, children: []
}
]
}
]
}
, { name: "b"
, children:
[ { name: "b.a"
, children:
[ { name: "b.a.a"
, children: []
}
, { name: "b.a.b"
, children: []
}
]
}
, { name: "b.b"
, children: []
}
]
}
, { name: "c"
, children: []
}
]
We can run transformAll on your data to transform all of the nodes -
transformAll (data)
// [ { name: 'a.a.a' }
// , { name: 'a.a.b', foo: 123 }
// , { name: 'b.a.a' }
// , { name: 'b.a.b' }
// , { name: 'b.b' }
// , { name: 'c' }
// ]
Or to transform a single node, we call transform1 -
transform1 (data[0])
// [ { name: 'a.a.a' }
// , { name: 'a.a.b', foo: 123 }
// ]
transform1 (data[2])
// [ { name: 'c' } ]
Expand the snippet below to verify the results in your own browser -
const data =
[ { name: "a"
, children:
[ { name: "a.a"
, children:
[ { name: "a.a.a"
, children: []
}
, { name: "a.a.b"
, foo: 123
, children: []
}
]
}
]
}
, { name: "b"
, children:
[ { name: "b.a"
, children:
[ { name: "b.a.a"
, children: []
}
, { name: "b.a.b"
, children: []
}
]
}
, { name: "b.b"
, children: []
}
]
}
, { name: "c"
, children: []
}
]
const transform1 = ({ children = [], ...node }) =>
children.length === 0 // leaf
? [ node ]
: children.length === 1 // singleton
? transform1 (...children)
: transformAll (children) // default
const transformAll = (arr = []) =>
arr .flatMap (transform1)
console .log (transformAll (data))
// [ { name: 'a.a.a' }
// , { name: 'a.a.b', foo: 123 }
// , { name: 'b.a.a' }
// , { name: 'b.a.b' }
// , { name: 'b.b' }
// , { name: 'c' }
// ]
Related
me and my partner have been cracking our heads at this. we have to create a tree, that obviously has "children" below it.
all we need right now, is to loop over an object to find a certain value, if that value is not in that certain object, then go into its child property and look there.
basically what I'm asking is, how can you loop over nested objects until a certain value is found?
would super appreciate the perspective of a more experienced coder on this.
/// this is how one parent with a child tree looks like right now,
essentially if we presume that the child has another child in the children property,
how would we loop into that?
and maybe if that child also has a child, so on and so on...
Tree {
value: 'Parent',
children: [ Tree { value: 'Child', children: [] } ]
}
You can use recursive function to loop through objects:
const Tree = {
value: 'Parent',
children: [
{
value: 'Child1',
children: [
]
},
{
value: 'Child2',
children: [
{
value: 'Child2.1',
children: [
{
value: 'Child2.1.1',
children: [
]
},
{
value: 'Child2.1.2',
children: [
]
},
]
},
]
},
]
}
function findValue(obj, value)
{
if (obj.value == value)
return obj;
let ret = null;
for(let i = 0; i < obj.children.length; i++)
{
ret = findValue(obj.children[i], value);
if (ret)
break;
}
return ret;
}
console.log("Child1", findValue(Tree, "Child1"));
console.log("Child2.1", findValue(Tree, "Child2.1"));
console.log("Child3", findValue(Tree, "Child3"));
You can try this simple solution:
const myTree = {
value: 'Parent',
children: [
{
value: 'Child1',
children: []
},
{
value: 'Child2'
}
]
}
const isValueInTree = (tree, findValue) => {
if (tree.value === findValue) return true;
if (tree.children && tree.children.length !== 0) {
for (const branch of tree.children) {
const valuePresents = isValueInTree(branch, findValue);
if (valuePresents) return true;
}
}
return false;
}
console.log(isValueInTree(myTree, 'Child2'));
console.log(isValueInTree(myTree, 'Child'));
console.log(isValueInTree(myTree, 'Child1'));
NOTE: This is the first question that I ever created in StackOverflow.
I was trying to implement layer manager in javascript with this value:
var layers = [
{
id: "layer_1",
name: "Layer 1",
child: [
{
id: "layer_1A",
name: "Layer 1A",
child: [
{
id: "layer_1A1",
name: "Layer 1A1"
},
{
id: "layer_1A2",
name: "Layer 1A2"
}
]
},
{
id: "layer_1B",
name: "Layer 1B"
}
]
},
{
id: "layer_2",
name: "Layer 2"
},
{
id: "layer_3",
name: "Layer 3"
}
]
and save the result into this:
var indexSelector = {};
here is the code:
var value = [-1];
function read_layers(obj) {
// Checks the array (layer/child)
if (Array.isArray(obj)) {
for (let layer of obj) {
read_layers(layer);
}
}
// Checks the object
else if (typeof obj == "object") {
for (let layer of Object.entries(obj)) {
// Checks the childs
if (layer[0] == "layers" || layer[0] == "child") {
value.push(-1);
read_layers(obj[layer[0]]);
value.pop()
}
// Checks the object keys
else if (layer[0] == "id") {
if (!indexSelector.hasOwnProperty(layer[1])) {
++value[value.length-1];
indexSelector[layer[1]] = value; // Result
}
}
}
}
}
read_layers(layers);
I want the expected result to looks like this:
{
layer_1: [ 0 ]
layer_1A: [ 0, 0 ]
layer_1A1: [ 0, 0, 0 ]
layer_1A2: [ 0, 0, 1 ]
layer_1B: [ 0, 1 ]
layer_2: [ 1 ]
layer_3: [ 2 ]
}
But here is the problem result:
{
layer_1: [ 2 ]
layer_1A: [ 2 ]
layer_1A1: [ 2 ]
layer_1A2: [ 2 ]
layer_1B: [ 2 ]
layer_2: [ 2 ]
layer_3: [ 2 ]
}
How to fix this problem with different object values? Thanks.
NOTE: All assignment (with operator = ) of objects is applied with theirs reference, in javascript, like in as java.
So, at line indexSelector[layer[1]] = value; // Result , the value is assigned with its reference, not the whole value.
That's why the values of the result are all same.
Solution: Use indexSelector[layer[1]] = Object.assign({}, value);.
It'll solve your problem.
data = [
{
name: "Parent Level 1",
questions: [
{
name: "question 1"
}
],
children: [
{
name: "Child 1 - P1",
questions: [
{
name: "ability to code"
},
{
name: "ability to do something"
}
],
children: [
{
name: "Child -2 P1",
questions: [
{
name: "figure out"
}
]
}
]
}
]
},
{
name : 'Parent Level 2',
questions : [
{name : 'question 1 P-2'}
]
},
{
name : 'Parent Level 3',
children: [
{
name : 'Child Level -1 P-3',
children: [
{
name : 'Child Level 2- P-3',
questions : [
{
name : 'Question level 2
}
]
}
]
questions: [
{name : 'hello there'}
]
}
]
}
];
Problem:
I need to perform a keyword search on the question, and if a question is found at a node - 3 let’s say then we need to return that node and all the parent nodes of that object.
For example, if I search for 'hello there', the final tree should be:
[
{
name : 'Parent Level 3',
children: [
{
name : 'Child Level -1 P-3',
children: [
{
name : 'Child Level 2- P-3',
questions : []
}
]
questions: [
{name : 'hello there'}
]
}
]
}
];
We can have children or questions [] at any node.
I am able to find the questions matching the search string, but I am not able to remove the unwanted nodes from the tree. Here is the code for that:
searchNode (data) {
for (let d of data) {
this.search(d)
}
}
search(data) {
let search = 'ability'
if(!!data.questions && data.questions.length > 0) {
data.questions = data.questions.filter((question) => {
return question.name.includes(search)
})
}
if(data.children && data.children.length > 0) {
searchNode(data.children)
}
}
search(data)
This should work for you. The demo code is in stackblitz. Check the result in the console.
Stackblitz demo
searchString = 'hello';
filteredData = [];
ngOnInit(): void {
this.filteredData = [];
this.data.forEach(node => {
if (this.checkQtn(node)) {
this.filteredData.push(node);
}
});
console.log(this.filteredData);
}
checkQtn(node): Boolean {
let response: Boolean = false;
if (node.questions) {
let qtns = [];
qtns = node.questions;
qtns.forEach(el => {
const eachQtn: string = el.name;
if (eachQtn.includes(this.searchString)) {
response = true;
}
});
}
if (!response && node.children) {
for (let i = 0; i < node.children.length && !response; i++) {
response = this.checkQtn(node.children[i]);
}
}
return response;
}
I've got some data that came from classy service in Angular that looks like this (briefly):
const obj = {
field: [
{
id: 1,
items: []
},
{
id: 2,
items: [ { wateva: 'wateva1' } ]
},
{
id: 3,
items: false
},
{
id: 4,
items: [ { yeah: 7 } ]
}
]
}
Well, my task is just to collect all array items, that are not empty.
My solution (actually my solution is written in TypeScript and Angular 5, but here to make it more simple and comprehensible it's going to be like...) :
function getItems() {
const items = [];
obj.field.forEach(currentField => {
if (currentField.items && currentField.items.length) {
currentField.items.forEach(currentItem => items.push(currentItem));
}
});
return items;
}
Right, it's dead simple and it works as expected (current one will return...) :
[ { wateva: 'wateva1' }, { yeah: 7 } ]
And now my question... How to make my solution functional? I want to get rid of my new variable items, I don't want to push in that variable, I just want to return the result in one action. Any help will be appreciated.
P.S. Suggestions with 3rd libraries are not accepted :)
If you can use es6 (and since you mentioned you're using typescript, that should be fine), you can turn this into a nice functional one-liner by combining concat, map, filter, and the spread operator:
const obj = {
field: [
{
id: 1,
items: []
},
{
id: 2,
items: [ { wateva: 'wateva1' } ]
},
{
id: 3,
items: false
},
{
id: 4,
items: [ { yeah: 7 } ]
}
]
}
function getItems(obj) {
return [].concat(...obj.field.map(o => o.items).filter(Array.isArray))
}
console.log(getItems(obj))
You can use flatMap (stage 3). flatMap here matches Fantasy Land's spec for chain.
data.field.flatMap
(({ items }) =>
Array.isArray (items) ? items : []
)
// [ { wateva: 'wateva1' }, { yeah: 7 } ]
You can polyfill it in environments that don't have it
Array.prototype.flatMap = function (f) {
return this.reduce
( (acc, x) =>
acc.concat (f (x))
, []
)
}
Full program demonstration
Array.prototype.flatMap = function (f) {
return this.reduce
( (acc, x) =>
acc.concat (f (x))
, []
)
}
const data =
{ field:
[ { id: 1, items: [] }
, { id: 2, items: [ { wateva: 'wateva1' } ] }
, { id: 3, items: false }
, { id: 4, items: [ { yeah: 7 } ] }
]
}
const result =
data.field.flatMap
(({ items }) =>
Array.isArray (items) ? items : []
)
console.log (result)
// [ { wateva: 'wateva1' }, { yeah: 7 } ]
You can use Array.reduce and the spread operator to accumulate onto an empty array:
obj.field.reduce(
(acc, current) => current.items && current.items.length > 0 ? [...acc, ...current.items] : acc, [])
);
Using Array.prototype.reduce, object destructuring, and spread assignments:
function getItems({ field }) {
return field.reduce((result, { items }) =>
items instanceof Array ?
items.reduce((items, item) => [...items, item], result) :
result
, []);
}
I am basically trying to convert a flat json file to tree view. Here the parent child relationship required for tree view is mentained by links key using source and target.
Here is the sample raw input:
{
"nodes" : [
{
name: "bz_db",
index: 0
},
{
name: "mysql",
index: 1
},
{
name: "postgres",
index: 2
},
{
name: "it-infra",
index: 3
},
{
name: "user-count",
index: 4
}
],
links: [
{
source: 0, target: 1
},
{
source: 0, target: 3
},
{
source: 1, target: 3
},
{
source: 3, target: 4
}
]
}
As you can see the link field maintains this relation ship, and finally I want my data in this format:
{
name: "bz_db",
children: [
{
name: "mysql",
children: [
{
name: "it-infra",
children: [
{
name: "user_count",
children: []
}
]
}
]
},
{
name: "it-infra",
children: [{
name: "user_count",
children: []
}
]
}
]
}
I tried to solve this, but it worked for 1 level (to show immediate child of a selected root element.
var findObjectByKeyValue = function(arrayOfObject, key, value){
return _.find(arrayOfObject, function(o){ return o[key] == value})
}
var rootObject = findObjectByKeyValue(sample_raw_input.nodes, 'name', 'bz_db');
var treeObject = {
name: rootObject.name,
index: rootObject.index,
root: true,
children: []
};
angular.forEach(dependencyData.links, function(eachLink){
if(treeObject.index == eachLink.source){
var rawChildObject = findObjectByKeyValue(dependencyData.nodes, 'index', eachLink.target);
var childObject = {};
childObject.index = rawChildObject.index;
childObject.name = rawChildObject.name;
childObject.children = [];
treeObject.children.push(childObject);
}
});
But the above code returns me only first level of depndencies, but i want hierarchical relationship.
I know i can use recursion here. But I am not so comfortable with it.
Josh's answer uses a sequence of map->filter->map->find calls, each of which iterate thru a collection of data. This loop of loop of loop of loops results in a stunning amount of computational complexity as the number of nodes in your collection increases.
You can dramatically simplify the creation of the tree by using a single reduce pass on each nodes and links. Map can also perform look-ups in logarithmic time, compared to Array's find which requires linear time (slower). When you consider this operation is called for each element of the input, it's clear to see a significant difference in time.
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
Lastly, we provide the MutableNode interface we relied upon
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
Below is a full program demonstration. JSON.stringify is used only to display the result
const MutableNode = (name, children = []) =>
({ name, children })
MutableNode.push = (node, child) =>
(node.children.push (child), node)
const makeTree = (nodes = [], links = []) =>
links.reduce
( (t, l) =>
t.set ( l.source
, MutableNode.push ( t.get (l.source)
, t.get (l.target)
)
)
, nodes.reduce
( (t, n) => t.set (n.index, MutableNode (n.name))
, new Map
)
)
.get (0)
const data =
{ nodes:
[ { name: "bz_db", index: 0 }
, { name: "mysql", index: 1 }
, { name: "postgres", index: 2 }
, { name: "it-infra", index: 3 }
, { name: "user-count", index: 4 }
]
, links:
[ { source: 0, target: 1 }
, { source: 0, target: 3 }
, { source: 1, target: 3 }
, { source: 3, target: 4 }
]
}
const tree =
makeTree (data.nodes, data.links)
console.log (JSON.stringify (tree, null, 2))
You can rely on tracking an object reference and do this without any recursion. Using Object.assign, map the list of nodes to its children:
// Assuming that input is in `input`
const nodes = input.nodes.reduce((a, node) => {
a[node.index] = { ...node, index: undefined };
return a;
}, []);
// organize the links by their source
const links = input.links.reduce((a, link) => {
return a.set((a.get(link.source) || []).concat(nodes[link.target]);
}, new Map());
// Apply side effect of updating node children
nodes.forEach(node => Object.assign(node, {
children: links.get(node.index),
}));
So I'm taking the list of nodes, and assigning to each (to mutate the node itself -- keep in mind this is a side-effect) a new array. Those children are all the links that link this node, and we Array#map them to convert their target ID into the actual node we want.
just share sample, a little different from yours.
But it give you a hint with recursive function.
jsFiddle flat array json transform to recursive tree json
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)
console.log(nested)