how to rearrange recursive json into tree structure with javascript? - javascript

I would like to transform the following JSON into another structure.
The source JSON:
values = array with objects wich needs to filtered by action === 'commented'
comment = object with the comment, n Tasks and n Comments
Comments can have endless more Comments and Tasks
{
"values": [
{
"action": "COMMENTED",
"comment": {
"text": "comment text",
"comments": [
{
"text": "reply text",
"comments": [],
"tasks": []
}
],
"tasks": [
{
"text": "task text",
"state": "RESOLVED"
}
]
}
}
]
}
The Target JSON:
Array(s) with Objects
each comment or tasks is a "children" (recursive!)
[
{
"text": "comment text",
"children": [
{
"text": "reply text",
"type": "comment"
},
{
"text": "task text",
"state": "RESOLVED"
}
]
}
]
Ive started with:
data = data.values.filter((e)=>{
return e.action === 'COMMENTED';
}).map((e)=>{
// hmmm recursion needed, how to solve?
});

data = data.values.filter(e => e.action === 'COMMENTED')
.map(function recursion({comment}){
return {
text: comment.text,
children: [...comment.comments.map(recursion), ...comment.tasks];
};
});

I ended up with:
let data = response.data.values
.filter(e => e.action === 'COMMENTED')
.map(function e({comment, commentAnchor}) {
return {
commentAnchor,
text: comment.text,
children: [...comment.comments.map(function recursion(comment) {
if (typeof comment === 'undefined') {
return {};
}
let children = [];
if (comment.comments) {
children.push(...comment.comments.map(recursion));
}
if (comment.tasks) {
children.push(...comment.tasks);
}
let _return = {
...comment,
text: comment.text
};
_return.children = children;
return _return;
}), ...comment.tasks]
}
});

Related

nested filter array javascript

I want to create a nested filter in js
when I filter my array primary data is affected and changed but I need preliminary data to remove filters
my js code :
let result = companies;
result.map((item, i) => {212
let rows = [...result[i].table.table_rows].filter((item3) => {
return Object.keys(item3).some(i => item3[i][key] === value[key]);
});
result[i].table.table_rows = [...rows];
return result[i];
});
arrayFilter(result);
my data is:
{
"companies": [
{
"company": {
"name": "company 1"
},
"table": {
"table_rows": [
{
"cells": {
"product_name": "prod1",
"pa_size": "12"
}
},
{
"cells": {
"product_name": "prod2",
"pa_size": "15"
}
}
]
}
},
{
"company": {
"name": "company 2"
},
"table": {
"table_rows": [
{
"cells": {
"product_name": "prod2-1",
"pa_size": "12"
}
},
{
"cells": {
"product_name": "prod2-2",
"pa_size": "18"
}
}
]
}
}
]
}
I tried many ways to solve this problem, but I did not get the right answer
Your question is not clear, the point I have understand that you wanted to filter the array "table_rows" located inside each companies array object? map and filter returns new array, so the solution for this is:
result = result.companies.map((item, i) => {
const newItem = {...item};
let rows = newItem .table.table_rows.filter((item3) => {
return Object.keys(item3).some(i => item3[i][key] === value[key]);
});
newItem.table_rows = [...rows];
return newItem ;
});
arrayFilter(result);

How to filter and map nested array items while checking whether the to be filtered items meet certain criteria?

I need to filter and map both, array items and each item's child array items while checking whether the to be filtered items have certain properties which meet certain criteria.
What are good array method based approaches for filtering and mapping nested array items?
Required Properties
"productName", "productCategory", "content", "id"
AND also if status inside productImages is not Linked or Existing
Sample Data
[
{
"productName": null,
"productCategory": null,
"productImages": [
{
"content": "1",
"id": null,
"data": null,
"file": null,
"url": null,
"status": "Existing"
},
{
"content": "",
"id": "234",
"data": "test data",
"file": "test file",
"url": null,
"status": "Existing"
}
]
},
{
"productName": null,
"productCategory": "hello category",
"productImages": [
{
"content": "1",
"id": null,
"data": null,
"file": null,
"url": null,
"status": "Existing"
},
{
"content": "",
"id": "234",
"data": "test data",
"file": "test file",
"url": null,
"status": "Existing"
}
]
},
{
"productName": "test product",
"productCategory": "test category",
"productImages": [
{
"content": "1",
"id": "123",
"data": "test data",
"file": "test file",
"url": null,
"status": "Linked"
},
{
"content": "2",
"id": "234",
"data": "test data",
"file": "test file",
"url": "test",
"status": "Linked"
}
]
},
{
"productName": "new product",
"productCategory": "new category",
"productImages": [
{
"content": "2",
"id": "32332",
"data": "test data",
"file": "test file",
"url": "test",
"status": "new"
}
]
}
]
Expected Output
[
{
"productIndex": 3,
"name": "new product",
"category": "new category",
"filteredImages": [
{
"newcontent": "2",
"id": "32332",
},
]
},
]
Code
const filteredProducts = products.filter((element) => element.productName && element.productCategory);
You can do it with one for-of loop:
const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
new_data.push({
productIndex: index,
name: item.productName,
category: item.productCategory,
filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id, }))
})
} else {
incomplete_items.push(index);
}
}
Working example
const data = [
{
"productName": null,
"productCategory": null,
"productImages": [
{ "content": "1", "id": null, "data": null, "file": null, "url": null, "status": "Existing" },
{ "content": "","id": "234","data": "test data", "file": "test file", "url": null, "status": "Existing" }
]
},
{
"productName": null,
"productCategory": "hello category",
"productImages": [
{ "content": "1", "id": null,"data": null, "file": null, "url": null, "status": "Existing"},
{ "content": "", "id": "234","data": "test data","file": "test file", "url": null, "status": "Existing" }
]
},
{
"productName": "test product",
"productCategory": "test category",
"productImages": [
{ "content": "1", "id": "123", "data": "test data", "file": "test file", "url": null, "status": "Linked" },
{ "content": "2", "id": "234", "data": "test data", "file": "test file", "url": "test","status": "Linked" }
]
},
{
"productName": "new product",
"productCategory": "new category",
"productImages": [
{ "content": "2","id": "32332", "data": "test data", "file": "test file", "url": "test", "status": "new" }
]
}
]
const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
new_data.push({
productIndex: index,
name: item.productName,
category: item.productCategory,
filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id, }))
})
} else {
incomplete_items.push(index);
}
}
if(incomplete_items.length) console.log(`Items ${incomplete_items} were missing required properties.`);
console.log(new_data);
You can use reduce for this.
Thank you Peter Seliger for pointing me to the double reduce. In terms of readability, do you think it's preferred to use 2 reducers instead of a filter().map() inside the outer reducer?
const sampleData = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
const expectedData = sampleData.reduce((acc, val, i) => {
// check if productName and productCategory are available
if (val.productName && val.productCategory) {
// get images that match the criteria and return object in desired format
const filteredImages = val.productImages.reduce(
(matchingImages, { content, id, status }) => {
if (content && id && status !== "Existing" && status !== "Linked") {
matchingImages.push({ newcontent: content, id });
}
return matchingImages;
},
[]
);
// if images are available, all conditions are met
if (filteredImages.length > 0) {
acc.push({
productIndex: i,
name: val.productName,
category: val.productCategory,
filteredImages,
});
}
}
return acc;
}, []);
console.log(expectedData);
.as-console-wrapper { min-height: 100%!important; top: 0; }
A straightforward approach would be based on just two reducer functions, each being properly named according to its implemented purpose, where the outer reduce task iterates the products and the inner task iterates each product's related images.
Thus one achieves both within each reduce task, the filtering and the property mapping for the expected result items.
const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
function collectNonMatchingImageItem(result, image) {
const { id = null, content = null, status = null } = image;
if (
// loose truthyness comparison ... 1st stage filter (valid image) ...
String(id ?? '').trim() &&
String(content ?? '').trim() &&
String(status ?? '').trim() &&
// ... 2nd stage filter (non matching valid image item) ...
!(/^linked|existing$/i).test(status)
) {
// ... and mapping at once.
result.push({
id,
newcontent: content,
});
}
return result;
}
function collectValidProductWithNonMatchingImages(result, product, idx) {
const {
productName = null,
productCategory = null,
productImages = [],
} = product;
// loose truthyness comparison ... 1st stage filter (valid product) ...
if (
String(productName ?? '').trim() &&
String(productCategory ?? '').trim()
) {
const nonMatchingItems = productImages
.reduce(collectNonMatchingImageItem, []);
// 2nd stage filter (valid product with non matching valid images) ...
if (nonMatchingItems.length >= 1) {
// ... and mapping at once.
result.push({
productIndex: idx,
name: productName,
category: productCategory,
filteredImages: nonMatchingItems,
});
}
}
return result;
}
const result = input
.reduce(collectValidProductWithNonMatchingImages, []);
console.log({ result });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Edit
"... How do you compare the old and new array. I wanted to output a warning if it isn't complete" – Joseph
The above approach shows its flexibility if it comes to the integration of the new requirement ...
const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
function collectNonMatchingImageItem(collector, image, idx) {
const { id = null, content = null, status = null } = image;
// loose truthyness comparison ... 1st stage filter (valid image) ...
if (
String(id ?? '').trim() &&
String(content ?? '').trim() &&
String(status ?? '').trim()
) {
// ... 2nd stage filter (non matching valid image item) ...
if (
!(/^linked|existing$/i).test(status)
) {
// ... and mapping at once.
collector.result.push({
id,
newcontent: content,
});
collector.nonMatchingAt.push(idx);
}
} else {
collector.invalidAt.push(idx);
}
return collector;
}
function collectValidProductWithNonMatchingImages(collector, product, idx) {
const {
productName = null,
productCategory = null,
productImages = [],
} = product;
// loose truthyness comparison ... 1st stage filter (valid product) ...
if (
String(productName ?? '').trim() &&
String(productCategory ?? '').trim()
) {
const {
result: nonMatchingItems,
nonMatchingAt,
invalidAt,
} = productImages.reduce(
collectNonMatchingImageItem,
{ result: [], nonMatchingAt: [], invalidAt: [] },
);
// report related variables.
let isPlural, itemPhrase, indexPhrase, imageReport;
// 2nd stage filter (valid product with non matching valid images) ...
if (nonMatchingItems.length >= 1) {
// ... and mapping at once.
collector.result.push({
productIndex: idx,
name: productName,
category: productCategory,
filteredImages: nonMatchingItems,
});
// create and collect report item.
isPlural = (nonMatchingAt.length >= 2);
itemPhrase = `item${ isPlural ? 's' : '' }`;
indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
imageReport = `with non matching valid image ${ itemPhrase } at ${ indexPhrase } ${ nonMatchingAt }`;
collector.report.push(
`Valid product item at index ${ idx } ${ imageReport }.`
);
}
if (invalidAt.length >= 1) {
// create and collect report item.
isPlural = (invalidAt.length >= 2);
itemPhrase = `item${ isPlural ? 's' : '' }`;
indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
imageReport = `with invalid image ${ itemPhrase } at ${ indexPhrase } ${ invalidAt }`;
collector.report.push(
`Valid product item at index ${ idx } ${ imageReport }.`
);
}
} else {
// create and collect report item.
collector.report.push(
`Invalid product item at index ${ idx }.`
);
}
return collector;
}
const {
result,
report,
} = input.reduce(
collectValidProductWithNonMatchingImages,
{ result: [], report: [] },
);
console.log({ result, report });
.as-console-wrapper { min-height: 100%!important; top: 0; }

Modify javascript object to specific format

let data = {
"rec": [{
"id": "25837",
"contentId": "25838"
},
{
"id": "25839",
"contentId": "25838"
},
{
"id": "25838"
},
{
"id": "25636",
"contentId": "25837"
}, {
"id": "25640",
"contentId": "25839"
}
]
};
I have a javascript object which I have to manipulate to below format.
{
"childern": [{
"id": "25838",
"childern": [{
"id": "25837",
"contentId": "25838",
"childern": [{
"id": "25636",
"contentId": "25837"
}]
},
{
"id": "25839",
"contentId": "25838",
"childern": [{
"id": "25640",
"contentId": "25839"
}]
}
]
}]
}
If any object dont have contentId it should be at parent level. then all the objects having contentId same as parent id should be at its child level and so on.
I have created a fiddle here but logic is not completed. Any idea or reference to achieve this.
You could create recursive function with reduce method to get the desired result.
let data = {"rec":[{"id":"25837","contentId":"25838"},{"id":"25839","contentId":"25838"},{"id":"25838"},{"id":"25636","contentId":"25837"},{"id":"25640","contentId":"25839"}]}
function nest(data, pid) {
return data.reduce((r, e) => {
if (pid == e.contentId) {
const obj = { ...e }
const children = nest(data, e.id);
if (children.length) obj.children = children
r.push(obj)
}
return r;
}, [])
}
const result = nest(data.rec);
console.log(result[0])

How to denormalize array in JS

I have a data set of the following form
let data = [
{
"id": {
"primary": "A1"
},
"msg": 1
}, {
"id": {
"primary": "A1"
},
"msg": 2
}, {
"id": {
"primary": "B2"
},
"msg": 3
}
]
I would like to transform it to
newData = [
{
"id": {
"primary": "A1"
},
"items": [
{ "msg": 1 },
{ "msg": 2 }
]
},
{
"id": {
"primary": "B2"
},
"items": [
{ "msg": 3 }
]
}
]
I think the method is something like the following, but am not sure how to check against undefined values in this case.
let newData = [];
for (let i = 0; i < data.length; i++) {
if (newData[i]['id']['primary'] === data[i]['id']) newData.push(data[i]['id'])
else newData[i]['items'].push(data[i]['msg'])
}
How can I transform the original data set to merge entries with a matching primary id?
One option would be to use .reduce() to create a new array from the existing.
I've added comments to clarify.
let data = [ { "id": { "primary": "A1" }, "msg": 1 }, { "id": { "primary": "A1" }, "msg": 2 }, { "id": { "primary": "B2" }, "msg": 3 } ];
let result = data.reduce((out,item) => {
let {id, ...items} = item; //Separate the "id" and "everything else"
let existing = out.find(({id}) => id.primary == item.id.primary);
existing //have we seen this ID already?
? existing.items.push(items) //yes - add the items to it
: out.push({ id: {...id}, items: [items]}); //no - create it
return out;
}, []);
console.log(result);
A couple notes:
You may notice that I've set the ID using id: {...id}, despite the id already being an object. This is because using the existing id object would create a reference, whereas {...id} creates a shallow copy.
I haven't specified the msg property anywhere. Instead, any properties that aren't id will be added to the items list (example below).
let data = [ { "id": { "primary": "A1" }, "msg": 1, "otherStuff": "Hello World!" }, { "id": { "primary": "A1" }, "msg": 2, "AnotherThing": true }, { "id": { "primary": "B2" }, "msg": 3, "someOtherProperty": false } ];
let result = data.reduce((out,item) => {
let {id, ...items} = item;
let existing = out.find(({id}) => id.primary == item.id.primary);
existing
? existing.items.push(items)
: out.push({ id: {...id}, items: [items]});
return out;
}, []);
console.log(result);
That said, if you start to nest objects (other than ID), they will likely be included as references; ...items is only a shallow copy.
If such a case, consider something like JSON.parse(JSON.stringify(...)) for a deep copy. Be sure to read the link though; there are caveats.
You could also solve this in a concise way via the Array.reduce and ES6 destructuring:
let data = [ { "id": { "primary": "A1" }, "msg": 1 }, { "id": { "primary": "A1" }, "msg": 2 }, { "id": { "primary": "B2" }, "msg": 3 } ]
let result = data.reduce((r, {id, msg}) =>
((r[id.primary] = r[id.primary] || { id, items: [] }).items.push({msg}), r), {})
console.log(Object.values(result))
In more readable format it is:
let data = [ { "id": { "primary": "A1" }, "msg": 1 }, { "id": { "primary": "A1" }, "msg": 2 }, { "id": { "primary": "B2" }, "msg": 3 } ]
let result = data.reduce((r, {id, msg}) => {
r[id.primary] = (r[id.primary] || { id, items: [] })
r[id.primary].items.push({msg})
return r
}, {})
console.log(Object.values(result))
The idea is to group by the id.primary and then once the grouping is done simply get the values via Object.values
Notice that this is one pass solution where you do not have to per each iteration do an Array.find against the current accumulator.

Manipulating javascript object with underscore

I have a Javascript object with a format like below
"items":
{
"Groups":[
{
"title":"group 1",
"SubGroups":[
{
"title":"sub1",
"id" : "1",
"items":[
{
"title":"Ajax request 1",
},
{
"title":"Ajax request 2",
}
]
},
{
"title":"sub2",
"id" : "2",
"items":[
{
"title":"Ajax request 3",
},
{
"title":"Ajax request 4",
}
]
}
]
}
]
There are n 'Groups', n 'subGroups' and n 'items'.
What I want to do firstly is get all the items from a particular group based on id. This is achieved using:
_.each(items.Groups, function(o) {
result = _.where(o.SubGroups, {
'id': '1'
});
});
which returns
"items":[{"title":"Ajax request 1",},{"title":"Ajax request 2",}]
Then I want to get the rest of the data, excluding the items and parent group I have just retrieved.
I tried this:
_.each(items.Groups, function(o) {
arr = _.without(o.SubGroups, _.findWhere(o.SubGroups, {id: '2'}));
});
But this only returns me the items like this:
{
"title":"sub2",
"id" : "2",
"items":[{"title":"Ajax request 3"},{"title":"Ajax request 4",}]
}
whereas what I need is this:
"items":
{
"Groups":[
{
"title":"group 1",
"SubGroups":[
{
"title":"sub2",
"id" : "2",
"items":[
{
"title":"Ajax request 3",
},
{
"title":"Ajax request 4",
}
]
}
]
}
]
Just try this:
_.each(items.Groups, function(o) {
arr = _.without(o, _.findWhere(o.SubGroups, {id: '2'}));
});
o should be enough => you want to get Groups and not SubGroups.
Following is a pure JS implementation:
JSFiddle.
var data = {
"Groups": [{
"title": "group 1",
"SubGroups": [{
"title": "sub1",
"id": "1",
"items": [{
"title": "Ajax request 1",
}, {
"title": "Ajax request 2",
}]
}, {
"title": "sub2",
"id": "2",
"items": [{
"title": "Ajax request 3",
}, {
"title": "Ajax request 4",
}]
}]
}]
}
var items = [];
var group = [];
data.Groups.forEach(function(o) {
var _tmp = JSON.parse(JSON.stringify(o));
_tmp.SubGroups = [];
o.SubGroups.forEach(function(s) {
if (s.id == "1") {
items.push(s.items);
} else {
_tmp.SubGroups.push(s);
group.push(_tmp)
}
});
});
function printObj(label, obj) {
document.write(label + "<pre>" + JSON.stringify(obj, 0, 4) + "</pre>")
}
printObj("group", group);
printObj("items", items);
Using underscore and using your logic to filter all subgroups:
//array to store subgroup with ID 1
var results = [];
var d = _.each(data.items.Groups, function(o) {
result = _.where(o.SubGroups, {
'id': '1'
});
//add to results array
results.push(result);
});
//make a clone of the earlier object so that you get the parent structure.
var data1 = _.clone(data);
//set the filtered results to the group
data1.items.Groups = results;
//your data as you want
console.log(data1)
Working code here

Categories

Resources