I'm a novice to recursion and I have a JSON structure with arrays of nested objects. Some of these objects have a boolean enabled: true. I'm trying to figure out how to extract the paths to all enabled objects and their children.
I tried both cleaning up the original object by removing unused paths but I got lost in accessing the parents. I also tried building a separate array of paths using dot-notation, as I can probably build a new nested object from that. My latest attempt at the dot-notation extract:
const sourceData = {
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Floors"
},
{ title: "Windows", enabled: true },
{ title: "Ceilings", enabled: true }
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
},
{
title: "Roof"
}
]
},
{
title: "Gardening"
}
]
};
function getEnabledPaths(level, acc) {
for (const tag of level.tags) {
if (tag.enabled) {
return tag.title;
} else if (tag.hasOwnProperty("tags")) {
var path = this.getEnabledPaths(tag);
if (path) acc.push(tag.title + "." + path);
}
}
return acc;
}
console.log(getEnabledPaths(sourceData, []));
I only get:
[
"Cleaning.Windows",
"Maintenance.Walls"
]
I would ideally end up with something like this:
[
'Work.Cleaning.Windows',
'Work.Cleaning.Ceilings',
'Work.Maintenance.Walls.Brickwall',
'Work.Maintenance.Walls.Wooden Wall'
]
In a perfect world (but I tried for days and went back to getting the dot notation results):
{
title: "Work",
tags: [
{
title: "Cleaning",
tags: [
{
title: "Windows",
enabled: true
},
{
title: "Ceilings",
enabled: true
}
]
},
{
title: "Maintenance",
tags: [
{
title: "Walls",
enabled: true,
tags: [
{
title: "Brickwall"
},
{
title: "Wooden wall"
}
]
}
]
}
]
};
The key to the recursion function is to both a) deal with children and b) the item itself.
Here's my take, which seems to work:
const sourceData = {title:"Work",tags:[{title:"Cleaning",tags:[{title:"Floors"},{title:"Windows",enabled:true},{title:"Ceilings",enabled:true}]},{title:"Maintenance",tags:[{title:"Walls",enabled:true,tags:[{title:"Brickwall"},{title:"Woodenwall"}]},{title:"Roof"}]},{title:"Gardening"}]};
function itemFilter(item) {
// enabled? done with this item
if (item.enabled) return item;
// not enabled and no tags? set to null
if (!item.tags) return null;
// filter all children, remove null children
item.tags = item.tags.map(child => itemFilter(child)).filter(child => child);
return item;
}
console.log(itemFilter(sourceData));
.as-console-wrapper {
max-height: 100vh !important;
}
You could pass enabled parameter down to lower levels of recursion if true value is found on some of the upper levels and based on that add path to the results or not.
const data ={"title":"Work","tags":[{"title":"Cleaning","tags":[{"title":"Floors"},{"title":"Windows","enabled":true},{"title":"Ceilings","enabled":true}]},{"title":"Maintenance","tags":[{"title":"Walls","enabled":true,"tags":[{"title":"Brickwall"},{"title":"Wooden wall"}]},{"title":"Roof"}]},{"title":"Gardening"}]}
function paths(data, prev = '', enabled = false) {
const result = [];
prev += (prev ? "." : '') + data.title;
if (!enabled && data.enabled) enabled = true;
if (!data.tags) {
if (enabled) {
result.push(prev);
}
} else {
data.tags.forEach(el => result.push(...paths(el, prev, enabled)))
}
return result;
}
const result = paths(data)
console.log(result)
Related
I'm trying to return a new array of objects from the nested properties of another object. This is my current object:
const teams = {
GRYFFINDOR: {
display: 'Gryffindor',
channel: ['#team-gryffindor'],
ui: 'lion',
},
HUFFLEPLUFF: {
display: 'Hufflepuff',
channel: ['#team-hufflepuff'],
ui: '', // empty string on purpose
},
SLYTHERIN: {
display: 'Slytherin',
channel: ['#team-slytherin'],
ui: 'snake',
},
}
Now, I'm trying to return an array of objects like so:
[
{ value: 'lion' },
{ value: '' },
{ value: 'snake' },
]
This is what I have tried:
const teamsUI = [Object.keys(teams).reduce((carry, item) => {
return {...carry, ...{ ['value']: teams[item].ui }}
}, {})];;
However, I get this:
console.log(teamsUI); // returns [ { value: 'snake' }]
What am I doing wrong?
You could do like this:
const teams = {
GRYFFINDOR: {
display: 'Gryffindor',
channel: ['#team-gryffindor'],
ui: 'lion',
},
HUFFLEPLUFF: {
display: 'Hufflepuff',
channel: ['#team-hufflepuff'],
ui: '', // empty string on purpose
},
SLYTHERIN: {
display: 'Slytherin',
channel: ['#team-slytherin'],
ui: 'snake',
},
}
const result = Object.values(teams).map(({ ui }) => ({ value: ui }));
console.log(result);
Beside nested result structure, you could take the values and destructure the wanted property formapping.
const
teams = { GRYFFINDOR: { display: 'Gryffindor', channel: ['#team-gryffindor'], ui: 'lion' }, HUFFLEPLUFF: { display: 'Hufflepuff', channel: ['#team-hufflepuff'], ui: '' }, SLYTHERIN: { display: 'Slytherin', channel: ['#team-slytherin'], ui: 'snake' } },
result = Object.values(teams).map(({ ui: value }) => ({ value }));
console.log(result);
Here is the shortest way to achieve what you want
Object.keys(teams).map(t=>({value: teams[t].ui}))
Now what are you doing wrong?
Take a look at this part
Object.keys(teams).reduce((carry, item) => {
return {...carry, ...{ ['value']: teams[item].ui }}
}, {});
You're reducing your object to a single value with the same key called value. This means that the value of teams[item].ui of the current object in the reduce function will override the previous and hence you'll end up with the last object of that reduce function. You're finally wrapping this last object in an array which gives you what you have.
A better way to achieve that will be to do something like this
Object.keys(teams).reduce((carry, item, index)=>{
carry[index] = { value: teams[item].ui };
return carry;
}, []);
i have JSON like below.
const testData = [
{
menu: 'Test',
submenu: [
{
menu: 'Test1',
submenu: [
{
menu: 'Test1.1',
},
{
menu: 'Test1.2',
},
{
secondLevel: [
{
menu: 'Test1.3',
submenu: [
{
menu: 'Test1.4',
},
{
menu: 'Test1.5',
},
],
},
],
},
],
},
i have used reduce function to traverse to search the expected word like Test1.1 i am getting proper value, whereas while search the Test1.4 is not coming up properly as it has secondLevel as parent object.
The code i used is below which is suggested in stackoverflow.
function search(data, value) {
return data.reduce((r, e) => {
const object = { ...e }
const result = search(e.submenu || [], value)
if (result.length) object.submenu = result
if (e.menu == value || result.length) r.push(object)
return r;
}, [])
}
Please suggest best way to search the element in secondLevel object as well. Thanks in advance
I have this problem where I cant really figure out how to implement this in code. I think that I will have to use to power of recursion to do this, but I am really struggling with it.
I have this object (simplified for this question), where the nested depths can be unknown.
const allValues = {
features: {
title: 'features',
description: 'some features',
fields: {
featureA: false,
featureB: true,
featureC: true
}
},
otherthings: {
title: 'otherthings',
description: 'otherthingsdescription',
fields: {
nestedotherthings: {
title: 'nestedotherthings',
description: 'nestedotherthingsdescription',
fields: {
againnested: {
title: 'againsnested',
description: 'againsnested',
fields: {
finallynestedvalue: 1
}
},
againnested2: {
title: 'againsnested2',
description: 'againsnested2',
fields: {
finallynestedvalue: 200
}
}
}
}
}
}
}
I want to run this object through some code and get an output like this:
const expected_output = {
features: {
featureA: false,
featureB: true,
featureC: true
},
othertings: {
nestedotherthings: {
againnested: {
finallynestedvalue: 1
},
againnested2: {
finallynestedvalue: 2
}
}
}
}
What I have tried:
const output = _(allValues).mapValues((value, key) => flattenFields({}, value, key)).value()
function flattenFields(parent, current, key) {
if (!current || !current.fields) {
return current
}
return {
...parent,
[key]: _(current.fields).mapValues((value, key) => flattenFields(current, value, key)).value()
}
}
I hope someone can help me with this and explain what I am doing wrong.
You could take a recursive approach by checking the values and if an object take the fields property or the value. Then rebuild a new object.
function convert(object) {
return Object.fromEntries(Object
.entries(object)
.map(([k, v]) => [k, v && typeof v === 'object' ? convert(v.fields) : v])
);
}
var data = { features: { title: 'features', description: 'some features', fields: { featureA: false, featureB: true, featureC: true } }, otherthings: { title: 'otherthings', description: 'otherthingsdescription', fields: { nestedotherthings: { title: 'nestedotherthings', description: 'nestedotherthingsdescription', fields: { againnested: { title: 'againsnested', description: 'againsnested', fields: { finallynestedvalue: 1 } }, againnested2: { title: 'againsnested2', description: 'againsnested2', fields: { finallynestedvalue: 200 } } } } } } },
result = convert(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
I've have a complex data structure with multiple nested arrays in place.
Below is the current structure
var contentData = {
data: {
content: [
{
type: "column",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text"
}
]
}
]
},
{
type: "acc-item",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text"
},
{
type: "ordered-item",
text: "Item 1"
},
{
type: "unordered-item",
text: "Item 2"
}
]
}
]
},
{
type: "acc-item",
sections: [
{
sub: [
{
type: "heading-1",
text: "Heading Text 2"
}
]
}
]
}
]
}
}
So What I wanted is,
I wanted to group all the ordered-item & unordered-item into a new object like {type: 'list', items:[all list items]}.
I need to extract all items which are inside sub and push it to new object embedded and it should placed in the root level like below,
{type:"acc-item",embedded:[{type:"heading-1",text:"Heading Text 2"}]};
So What I've done so far,
I can able to group acc-item, but not the ordered-item & unordered-item.
So my final expected result should like this,
[{
"type": "column",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text"
}
]
},
{
"type": "acc-group",
"items": [
{
"type": "acc-item",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text"
},
{
"type": "list",
"items": [
{
"type": "ordered-item",
"text": "Item 1"
},
{
"type": "unordered-item",
"text": "Item 2"
}
]
}
]
},
{
"type": "acc-item",
"embedded": [
{
"type": "heading-1",
"text": "Heading Text 2"
}
]
}
]
}]
Below is my code,
var group,contentData={data:{content:[{type:"column",sections:[{sub:[{type:"heading-1",text:"Heading Text"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text"},{type:"ordered-item",text:"Item 1"},{type:"unordered-item",text:"Item 2"}]}]},{type:"acc-item",sections:[{sub:[{type:"heading-1",text:"Heading Text 2"}]}]}]}},types=[["list",["ordered-item","unordered-item"]],["accordion",["acc-item"]]];
var result = contentData.data.content.reduce((r, o) => {
var type = (types.find(({ 1: values }) => values.indexOf(o.type) > -1)|| {})[0];
if (!type) {
r.push(o);
group = undefined;
return r;
}
if (!group || group.type !== type) {
group = { type, items: [] };
r.push(group);
}
group.items.push(o);
return r;
}, []);
document.body.innerHTML = '<pre>' + JSON.stringify(result, null, ' ') + '</pre>';
You could store the last items array as well as the last embedded array and use them until a column type is found.
var contentData = { data: { content: [{ type: "column", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text" }, { type: "ordered-item", text: "Item 1" }, { type: "unordered-item", text: "Item 2" }] }] }, { type: "acc-item", sections: [{ sub: [{ type: "heading-1", text: "Heading Text 2" }] }] }] } },
list = ["ordered-item", "unordered-item"],
lastItems, lastEmbedded,
result = contentData.data.content.reduce((r, { type, sections }) => {
if (type === 'column') {
r.push({ type, embedded: sections.reduce((q, { sub }) => q.concat(sub), []) });
lastItems = undefined;
lastEmbedded = undefined;
return r;
}
if (!lastItems) r.push({ type: "acc-group", items: lastItems = [] });
lastItems.push(...sections.map(({ sub }) => ({
type,
embedded: sub.reduce((q, o) => {
if (list.includes(o.type)) {
if (!lastEmbedded) q.push({ type: 'list', items: lastEmbedded = [] });
lastEmbedded.push(o);
} else {
q.push(o);
lastEmbedded = undefined;
}
return q;
}, [])
})));
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
The Array.prototype and Object.prototype methods are perfect for this kind of thing.
And you're right that this is some complicated kind of logic.
I would suggest that you definitely need some unit tests for this, and try break in to separate pieces.
Here's how I'm thinking I'd do it.
1. Group By the type to create your groups..
I'm actually creating a more generic solution that you've asked for here. That is, I'm not just grouping the 'acc-item', but everything.
I did a quick search for 'array group by javascript' and it gives us this answer which suggests using Array.reduce, so let's do that.
const groupedData = contentData.data.content.reduce((acc, cur) => {
//Check if this indexed array already exists, if not create it.
const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];
return {
...acc,
[`${cur.type}-group`]: {
type: `${cur.type}-group`,
items: [...currentArray, cur]
}
}
}, {});
2. Now for each of those items, we need to look at their subs, and group just the list items.
To do this, we basically want to find all the `item -> sections -> sub -> types and filter them into two arrays. A quick google on how to create two arrays using a filter gives me this answer.
First though, we need to flatten that sections-> subs thing, so lets just do that.
function flattenSectionsAndSubs(item) {
return {
type: item.type,
subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
};
}
And I'll just copy paste that partition function in:
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [[...pass, elem], fail] : [pass, [...fail, elem]];
}, [[], []]);
}
const listTypes = ['ordered-item', 'unordered-item'];
function createEmbeddedFromItem(item) {
const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type);
return {
type: item.type,
embedded: [
...nonLists,
{
type: "list",
items: lists
}
]
}
}
Putting this all together and we get.
const contentData = {
data: {
content: [{
type: "column",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text"
}]
}]
},
{
type: "acc-item",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text"
},
{
type: "ordered-item",
text: "Item 1"
},
{
type: "unordered-item",
text: "Item 2"
}
]
}]
},
{
type: "acc-item",
sections: [{
sub: [{
type: "heading-1",
text: "Heading Text 2"
}]
}]
}
]
}
}
function partition(array, isValid) {
return array.reduce(([pass, fail], elem) => {
return isValid(elem) ? [
[...pass, elem], fail
] : [pass, [...fail, elem]];
}, [
[],
[]
]);
}
function flattenSectionsAndSubs(item) {
return {
type: item.type,
subs: item.sections.reduce((acc, cur) => ([...acc, ...cur.sub]), [])
};
}
const listTypes = ['ordered-item', 'unordered-item'];
function createEmbeddedFromItem(item) {
const [lists, nonLists] = partition(item.subs, (v) => listTypes.includes(v.type));
return {
type: item.type,
embedded: [
...nonLists,
{
type: "list",
items: lists
}
]
}
}
const groupedData = contentData.data.content.reduce((acc, cur) => {
//Check if this indexed array already exists, if not create it.
const currentArray = (acc[`${cur.type}-group`] && acc[`${cur.type}-group`].items) || [];
const flattenedItem = flattenSectionsAndSubs(cur);
const embeddedItem = createEmbeddedFromItem(flattenedItem);
return {
...acc,
[`${cur.type}-group`]: {
type: `${cur.type}-group`,
items: [...currentArray, embeddedItem]
}
}
}, {});
console.log(groupedData);
Now this doesn't exactly match what you've asked for - but it should probably work.
You can add your own bits into only add a list item, if the array isn't empty, and to stop the column from being in its own group.
The thing is - tbh it seems like a little bit of a red flag that you would create an array of items that don't having matching structures, which is why I've done it this way.
I'm certain this question is very common, but I can't seem to find a robust answer for my use case.
I have an Array of objects with nesting in two levels. Here is an example of the array:
let array = [
{ company: 'CompanyName1',
child: [
{ title: 'title1a',
baby: [
{ title: 'title1ab' },
{ title: 'title1abc' }
]
},
{ title: 'title2a',
baby: [
{ title: 'titleb2abcd' },
{ title: 'titleb2abcde' }
]
}
]
},
{ company: 'CompanyName2',
child: [
{ title: 'title2b',
baby: [
{ title: 'titleb3ab' },
{ title: 'titleb3abc' }
]
}
]
}
]
And this is my expected Array:
let newArray = [
{
company: 'companyName1',
child_title_0: 'title1a',
child_title_1: 'title1a',
child_baby_0: 'title1ab',
child_baby_1: 'title1abc',
child_baby_2: 'title1abcd',
child_baby_3: 'title1abcde',
},
{
company: 'companyName2',
child_title_0: 'title2b',
child_baby_0: 'titleb3ab',
child_baby_1: 'titleb3abc',
}
]
Basically I need to flatten each of the top level objects of the array. Since the nested objects have the same keys (follow a model, and are dynamic -- some items have 10 nested objects, some 0, etc.) I have to dynamically generate each of the new keys, possibly based in the index of the loops.
Any help -- direction is appreciated.
Thanks!
You can use the map function to return a manipulated version of each object in the array.
let results = [
{
company: 'CompanyName1',
child: [
{
title: 'title1a',
baby: [
{ title: 'title1ab' },
{ title: 'title1abc' }
]
},
{
title: 'title2a',
baby: [
{ title: 'titleb2abcd' },
{ title: 'titleb2abcde' }
]
}
]
},
{
company: 'CompanyName2',
child: [
{
title: 'title2b',
baby: [
{ title: 'titleb3ab' },
{ title: 'titleb3abc' }
]
}
]
}
];
let flattened = results.map(company => {
let childCount = 0, babyCount = 0;
company.child.forEach(child => {
company['child_title_'+childCount] = child.title;
child.baby.forEach(baby => {
company['child_baby_'+babyCount] = baby.title;
babyCount++;
});
childCount++;
});
delete company.child;
return company;
});
console.log(flattened);