How can I iteratively render nested components in react?
(as I understood, recursion is more expensive in terms of performance?)
P.S. nesting levels can be unlimited
Example:
"comments": [
{
"name": "1comment",
"body": "1comment",
"date": "2019-05-15T15:56:15.694116Z",
"id": "0179ef41-fdb6-4700-a4dc-6d7bbc54385a",
"parent": null,
"reply": []
},
{
"name": "2comment",
"body": "2comment",
"date": "2019-05-17T13:59:51.167188Z",
"id": "1ef06878-58b5-48b0-9349-73986ab66bb4",
"parent": null,
"reply": [
{
"name": "2-1-comment",
"body": "2-1-comment",
"date": "2019-05-21T22:32:44.998207Z",
"id": "514aa634-08bd-4ca3-8a1a-eb10846808ed",
"parent": "5a01211d-3ee9-4bf6-9a50-462a8277898a",
"reply": [
{
"name": "2-1-1-comment",
"body": "2-1-1-comment",
"date": "2019-05-21T22:32:44.998207Z",
"id": "514aa634-08bd-4ca3-8a1a-eb10846808ed",
"parent": "5a01231d-3119-4bf6-9a50-462a8277898a",
"reply": []
}
]
}
]
},
{
"name": "3comment",
"body": "3comment",
"date": "2019-05-19T12:07:15.613266Z",
"id": "5a01231d-3ee9-4bf6-9a50-462a8277898a",
"parent": null,
"reply": [
{
"name": "3-1-comment",
"body": "3-1-comment",
"date": "2019-05-21T22:32:44.998207Z",
"id": "514aa634-08bd-4ca3-8a1a-eb10846808ed",
"parent": "5a01231d-3ee9-4bf6-9a50-462a8277198a",
"reply": []
}
]
}
]
The basic principle would be to store the "path" to the current element in some way, then
push if there is a child,
iterate over current list,
pop if the current list is done.
E.g. stack = [ 2, 1, 3 ] in your case would represent element.reply[ 2 ].reply[ 1 ].reply[ 3 ].
I believe you have to store as well the lengths of all the lists of each path (to be able to iterate every level), which could be stored directly in the stack, but that in turn makes it more confusing to get back the index when you need it.
E.g.:
const stack = [ comments.length ];
while( stack.length > 0 ){
// -- get current item by the "path" (i.e. by the stack)
const currentItem = stack.reduce( (acc, value, index ) => {
const list = index === 0 ? acc : acc.reply; // root or not root
return list[ list.length - value ];
}, comments);
// --
if( currentItem ){
console.log( stack, currentItem && currentItem.name );
if ( currentItem.reply && currentItem.reply.length > 0 ) {
stack.push(currentItem.reply.length); // level deeper
} else {
stack[stack.length - 1]--; // next sibling
}
} else {
stack.pop(); // level up
stack[stack.length - 1]--; // next sibling
}
}
About the performance:
I guess the iterative approach becomes necessary only for really huge amounts of data. For 'normal' data the recursion should be able to keep a structure of 5 or even 50 levels deep in memory. E.g. I used the iterative approach once for millions of XML lines (not limited, so theoretically infinite), deeply nested. I guess for thousands I would still have used recursion.
Tip: Do you use a pattern like acc = [ ...acc, item ] (whick is common in React) ? That becomes slow at a scale of hundreds or thousands. Then you might want to have a look at Is mutating accumulator in reduce function considered bad practice
Related
Backend sends an object that contains an array of objects. These objects contain more arrays of objects and so on. It resembles a tree.
I need to be able to go from one object to the other, following the array, and back. What would be the best way to do this in typescript?
I tried forEach, but I couldn't go back. For cycles inside of for cycles aren't an option either because sometimes there will be 2 levels of arrays, sometimes, 5. I thought of an iterator, but I don't know enough of angular/typescript to make it happen.
Here is a snippet of the data. This is a questionnaire and I need to show each question individually.
"questionId": 1,
"parent": null,
"description": "Question 1?",
"children":
[
{
"questionId": 2,
"parent": 1,
"description": "Question 2?",
"children":
[
{
"questionId": 4,
"parent": 2,
"description": "Question 4?",
"children": []
}
]
},
{
"questionId": 3,
"parent": 1,
"description": "Question 3?",
"children": []
}
]
Sorry if I'm explaining it poorly or something is missing, I'm not used to post here.
If you just want to iterate through all the question objects, you could try to flatten your data with a recursive function like this,
const flattenQs = (qData) => {
const flattenedQs = []
flattenedQs.push({questionId: qData.questionId, parent: qData.parent, description: qData.description})
for (let i = 0; i < qData.children.length; i++) {
const qChild = qData.children[i];
flattenedQs.push(...flattenQs(qChild))
}
return flattenedQs
}
Which would give something like this,
[
{
questionId:1,
parent:null,
description:"Question 1?"
},
{
questionId:2,
parent:1,
description:"Question 2?"
},
{
questionId:4,
parent:2,
description:"Question 4?"
},
{
questionId:3,
parent:1,
description:"Question 3?"
}
]
I am trying in Javascript, using PUG template (if possible), to compare two arrays and when I find a correspondance in IDs, display some particular elements.
// First Array : I iterate over "hearts" object
// Called in PUG : - const user
[
{
"hearts": [
"5e70c63a94b27b164c9b897f",
"5e723c75e4bfdf4f58c55e32"
],
"_id": "5e6bb1189978fd5afc98c57a",
"email": "catherine#catherine.com",
"name": "Catherine",
"photo": "0121b7fe-b2ae-4e75-979d-7dea1a432855.jpeg",
"__v": 0
},
{
"hearts": [
"5e723c75e4bfdf4f58c55e32"
],
"_id": "5e6bc41f5915e3d2980a5174",
"email": "marc#marc.com",
"name": "Marc",
"photo": "4caa7bfb-6408-4893-a78b-fa6e8e5b03e7.png",
"__v": 0
}
]
// Second array : I iterate over "author.hearts" object
// Called in PUG : - const store
[{
"product": {
"categories": [
1,
2
]
},
"_id": "5e6bcc76c4022eae00e22af6",
"date": "2222-02-20T21:22:00.000Z",
"author": {
"hearts": [
"5e723c75e4bfdf4f58c55e32",
"5e70c63a94b27b164c9b897f"
],
"_id": "5e6bb1189978fd5afc98c57a",
"__v": 0
},
"created": "2020-03-13T18:09:58.086Z",
"id": "5e6bcc76c4022eae00e22af6"
}]
I want to loop over the first array, find the first ID (here 5e70c63a94b27b164c9b897f), loop over the second array and see if this ID is present within the "author.hearts" object. If it is not, carry on with the second ID and if it is present, display all the keys (tags, photos, _id, date...) from the object where the ID was found.
In my example, I have just one object in my array, but I'll be having much more later on.
Many thanks for your help
If I'm understanding correctly you can do something like this. Loop through all your users and when you find their id in author.hearts stop the loop there and return the object the user's _id was found in.
var resultFound = undefined;
try {
user.forEach((el) => {
const id = el._id;
const result = store.find(el => el.author.hearts.includes(id));
if (result) {
resultFound = result;
throw resultFound;
}
});
} catch (e) {
if (e !== resultFound) {
throw e;
}
}
I have a JSON that has three levels and it's stored as Array I need to get 2nd level(count started from 0) and below in each level has 10+ elements. How it can be implemented on JavaScript.Need your help. All response will be helpful.
ps. some 1st level and 2nd level elements can be empty
[
{
"name": "0th level first", //0th level
"options": [
{
"name": "1st level Cafe", // 1st level
"options": [
{
"name": "2nd level Staff", //2nd level
"options": [
{
"name": "Gary", //3rd level
"age": 18
},
{
"name": "James", //3rd level
"age": 25
}
]
}
]
}
]
}
]
Probably this one? :)
data.forEach((value, index) => {
for(stage0 in value){
if(typeof value[stage0] === 'object'){
value[stage0].forEach((val, index) => {
for(stage1 in val){
if(typeof val[stage1] === 'object'){
val[stage1].forEach((val2, index) => {
for(stage2 in val2){
console.log(val2[stage2]);
}
})
}
}
})
}
}
})
You can use array.forEach which will be only iterating through each level.
on first loop there is a property call options which is an array, you need to loop through the options array in 0 level, on the second loop again one more options array comes you need to again loop through the options array which is 1 level.
then you reach the thrid level which is your output.
Third level means starting from zero its second level.
I hope this will solve the issue.
var data = [
{
"name": "0th level first", //0th level
"options": [
{
"name": "1st level Cafe", // 1st level
"options": [
{
"name": "2nd level Staff", //2nd level
"options": [
{
"name": "Gary", //3rd level
"age": 18
},
{
"name": "James", //3rd level
"age": 25
}
]
}
]
}
]
}
]
data.forEach(fl => {
fl.options.forEach(sl => {
sl.options.forEach(tl => {
console.log("second level starting from 0",tl)
})
})
})
Assume data contains your json, we have 2 onliners:
let out = data[0].options[0].options[0];
let outArr = data.flatMap(x=>x.options.flatMap(y=>y.options));
results
// out = {"name":"2nd level Staff","options":[{"name":"Gary","age":18},{"name":"James","age":25}]}
// outArr = [{"name":"2nd level Staff","options":[{"name":"Gary","age":18},{"name":"James","age":25}]}]
First one (out) contains one 2nd element. We use here tree indexes here (three zeros) first data[0] return first element in array data, then options[0] return first element in options array.
The second solution (outArr) contains array of all 2nd elements. We use here JS flatMap
To write second solution inspired me abdullahkady comment below question.
I want to create a array structure with child entities like this ->
$scope.groups = [
{
"categories": [
{
"name": "PR",
"sortOrder": 0,
"type": "category"
}
],
"name": "DEPT 1",
"sortOrder": 0,
"type": "group",
"id": "-JY_1unVDQ5XKTK87DjN",
"editing": false
}
];
from an array that dosen't have child entities but all the items are listed in one object like this->
$scope.groups = [
{
"name": "PR",
"sortOrder": 0,
"type": "category"
},
{
"name": "AD",
"sortOrder": 3,
"type": "category"
},
{
"name": "DEPT 2",
"sortOrder": 1,
"type": "group",
"id": "-JYZomQKCVseJmaZoIF9",
"editing": false,
"categories": []
},
];
Is there any possible way?
As #Eagle1 has rightly pointed out. You need to define your data model properly to define a function that does that grouping for you. That said, from what I understand you have a $scope.groups array of objects for a specific department containing some categories which you need to consolidate as a child element.
You could start by defining a function that returns an object like you mention:
var organize = function(arr){
cats = [];
dep = {};
$.each( arr, function( i, val ) {
if(val.type == "category")
cats.push(val);
else
dep = val;
});
dep.categories = cats;
return dep;
}
Ultimately, you'll have to traverse the array and look for objects of type category and dump them in an array and have that array as the categories key of the object that you intend to return. I hope it gets you started in the right direction.
of course it is.
It's doable in javascipt although to help you devise something we would need a relationship between categories.
However, that's sounds like something that should be done in your data model (a relationship between dept - category, classic reflexive relationship parent - children). angular should be receiving from the back end an array already ordered.
I have a quick question on map-reduce with mongodb. I have this following document structure
{
"_id": "ffc74819-c844-4d61-8657-b6ab09617271",
"value": {
"mid_tag": {
"0": {
"0": "Prakash Javadekar",
"1": "Shastri Bhawan",
"2": "Prime Minister's Office (PMO)",
"3": "Narendra Modi"
},
"1": {
"0": "explosion",
"1": "GAIL",
"2": "Andhra Pradesh",
"3": "N Chandrababu Naidu"
},
"2": {
"0": "Prime Minister",
"1": "Narendra Modi",
"2": "Bharatiya Janata Party (BJP)",
"3": "Government"
}
},
"total": 3
}
}
when I am doing my map reduce code on this collection of documents I want to specify total as the sort field in this command
db.ana_mid_big.mapReduce(map, reduce,
{
out: "analysis_result",
sort: {"value.total": -1}
}
);
But this does not seem to work. How can I specify a key which is nested for sorting? Please help.
----------------------- EDIT ---------------------------------
as per the comments I am posting my whole problem here. I have started with a collection with a little more than 3.5M documents (this is just an old snap shot of the live one, which already crossed 5.5 M) which looks like this
{
"_id": ObjectId("53b394d6f9c747e33d19234d"),
"autoUid": "ffc74819-c844-4d61-8657-b6ab09617271"
"createDate": ISODate("2014-07-02T05:12:54.171Z"),
"account_details": {
"tag_cloud": {
"0": "FIFA World Cup 2014",
"1": "Brazil",
"2": "Football",
"3": "Argentina",
"4": "Belgium"
}
}
}
So, there can be many documents with the same autoUid but with different (or partially same or even same) tag_cloud.
I have written this following map-reduce to generate an intermediate collection which looks like the one at the start of the question. So, evidently that is collection of all the tag_clouds belongs to one person in a single document. To achieve this the MR code i used looks like the following
var map = function(){
final_val = {
tag_cloud: this.account_details.tag_cloud,
total: 1
};
emit(this.autoUid, final_val)
}
var reduce = function(key, values){
var fv = {
mid_tags: [],
total: 0
}
try{
for (i in values){
fv.mid_tags.push(values[i].tag_cloud);
fv.total = fv.total + 1;
}
}catch(e){
fv.mid_tags.push(values)
fv.total = fv.total + 1;
}
return fv;
}
db.my_orig_collection.mapReduce(map, reduce,
{
out: "analysis_mid",
sort: {createDate: -1}
}
);
Here comes problem Number-1 when somebody has more than one record it obeys reduce function. But when somebody has only one instead of naming it "mid_tag" it retains the name "tag_cloud". I understand that there is some problem with the reduce code but can not find what.
Now I want to reach to a final result which looks like
{"_id": "ffc74819-c844-4d61-8657-b6ab09617271",
"value": {
"tags": {
"Prakash Javadekar": 1,
"Shastri Bhawan": 1,
"Prime Minister's Office (PMO)": 1,
"Narendra Modi": 2,
"explosion": 1,
"GAIL": 1,
"Andhra Pradesh": 1,
"N Chandrababu Naidu": 1,
"Prime Minister": 1,
"Bharatiya Janata Party (BJP)": 1,
"Government": 1
}
}
Which is finally one document for each person representing the tag density they have used. The MR code I am trying to use (not tested yet) looks like this---
var map = function(){
var val = {};
if ("mid_tags" in this.value){
for (i in this.value.mid_tags){
for (j in this.value.mid_tags[i]){
k = this.value.mid_tags[i][j].trim();
if (!(k in val)){
val[k] = 1;
}else{
val[k] = val[k] + 1;
}
}
}
var final_val = {
tag: val,
total: this.value.total
}
emit(this._id, final_val);
}else if("tag_cloud" in this.value){
for (i in this.value.tag_cloud){
k = this.value.tag_cloud[i].trim();
if (!(k in val)){
val[k] = 1;
}else{
val[k] = val[k] + 1;
}
}
var final_val = {
tag: val,
total: this.value.total
}
emit(this._id, final_val);
}
}
var reduce = function(key, values){
return values;
}
db.analysis_mid.mapReduce(map, reduce,
{
out: "analysis_result"
}
);
This last piece of code is not tested yet. That is all I want to do. Please help
Your PHP background appears to be showing. The data structures you are representing are not showing arrays in typical JSON notation, however there are noted calls to "push" in your mapReduce code that at least in your "interim document" the values are actually arrays. You seem to have "notated" them the same way so it seems reasonable to presume they are.
Actual arrays are your best option for storage here, especially considering your desired outcome. So even if they do not, your original documents should look like this, as they would be represented in the shell:
{
"_id": ObjectId("53b394d6f9c747e33d19234d"),
"autoUid": "ffc74819-c844-4d61-8657-b6ab09617271"
"createDate": ISODate("2014-07-02T05:12:54.171Z"),
"account_details": {
"tag_cloud": [
"FIFA World Cup 2014",
"Brazil",
"Football",
"Argentina",
"Belgium"
]
}
}
With documents like that or if you change them to be like that, then your right tool for doing this is the aggregation framework. That works in native code and does not require JavaScript interpretation, hence it is much faster.
An aggregation statement to get to your final result is like this:
db.collection.aggregate([
// Unwind the array to "de-normalize"
{ "$unwind": "$account_details.tag_cloud" },
// Group by "autoUid" and "tag", summing totals
{ "$group": {
"_id": {
"autoUid": "$autoUid",
"tag": "$account_details.tag_cloud"
},
"total": { "$sum": 1 }
}},
// Sort the results to largest count per user
{ "$sort": { "_id.autoUid": 1, "total": -1 }
// Group to a single user with an array of "tags" if you must
{ "$group": {
"_id": "$_id.autoUid",
"tags": {
"$push": {
"tag": "$_id.tag",
"total": "$total"
}
}
}}
])
Slightly different output, but much simpler to process and much faster:
{
"_id": "ffc74819-c844-4d61-8657-b6ab09617271",
"tags": [
{ "tag": "Narendra Modi", "total": 2 },
{ "tag": "Prakash Javadekar", "total": 1 },
{ "tag": "Shastri Bhawan", "total": 1 },
{ "tag": "Prime Minister's Office (PMO)", "total": 1 },
{ "tag": "explosion", "total": 1 },
{ "tag": "GAIL", "total": 1 },
{ "tag": "Andhra Pradesh", "total": 1 },
{ "tag": "N Chandrababu Naidu", "total": 1 },
{ "tag": "Prime Minister", "total": 1 },
{ "tag": "Bharatiya Janata Party (BJP)", "total": 1 },
{ "tag": "Government", "total": 1 }
]
}
Also sorted by "tag relevance score" for the user for good measure, but you can look at dropping that or even both of the last stages as is appropriate to your actual case.
Still, by far the best option. Get to learn how to use the aggregation framework. If your "output" will still be "big" ( over 16MB ) then try to look at moving to MongoDB 2.6 or greater. Aggregate statements can produce a "cursor" which can be iterated rather than pull all results at once. Also there is the $out operator which can create a collection just like mapReduce does.
If your data is actually in the "hash" like format of sub-documents how you indicate in your notation of this ( which follows a PHP "dump" convention for arrays ), then you need to use mapReduce as the aggregation framework cannot traverse "hash-keys" the way these are represented. Not the best structure, and you should change it if this is the case.
Still there are several corrections to your approach and this does in fact become a single step operation to the final result. Again though, the final output will contain and "array" of "tags", since it really is not good practice to use your "data" as "key" names:
db.collection.mapReduce(
function() {
var tag_cloud = this.account_details.tag_cloud;
var obj = {};
for ( var k in tag_cloud ) {
obj[tag_cloud[k]] = 1;
}
emit( this.autoUid, obj );
},
function(key,values) {
var reduced = {};
// Combine keys and totals
values.forEach(function(value) {
for ( var k in value ) {
if (!reduced.hasOwnProperty(k))
reduced[k] = 0;
reduced[k] += value[k];
}
});
return reduced;
},
{
"out": { "inline": 1 },
"finalize": function(key,value) {
var output = [];
// Mapped to array for output
for ( var k in value ) {
output.push({
"tag": k,
"total": value[k]
});
}
// Even sorted just the same
return output.sort(function(a,b) {
return ( a.total < b.total ) ? -1 : ( a.total > b.total ) ? 1 : 0;
});
}
}
)
Or if it actually is an "array" of "tags" in your original document but your end output will be too big and you cannot move up to a recent release, then the initial array processing is just a little different:
db.collection.mapReduce(
function() {
var tag_cloud = this.account_details.tag_cloud;
var obj = {};
tag_cloud.forEach(function(tag) {
obj[tag] = 1;
});
emit( this.autoUid, obj );
},
function(key,values) {
var reduced = {};
// Combine keys and totals
values.forEach(function(value) {
for ( var k in value ) {
if (!reduced.hasOwnProperty(k))
reduced[k] = 0;
reduced[k] += value[k];
}
});
return reduced;
},
{
"out": { "replace": "newcollection" },
"finalize": function(key,value) {
var output = [];
// Mapped to array for output
for ( var k in value ) {
output.push({
"tag": k,
"total": value[k]
});
}
// Even sorted just the same
return output.sort(function(a,b) {
return ( a.total < b.total ) ? -1 : ( a.total > b.total ) ? 1 : 0;
});
}
}
)
Everything essentially follows the same principles to get to the end result:
De-normalize to a "user" and "tag" combination with "user" and the grouping key
Combine the results per user with a total on "tag" values.
In the mapReduce approach here, apart from being cleaner than what you seemed to be trying, the other main point to consider here is that the reducer needs to "output" exactly the same sort of "input" that comes from the mapper. The reason is actually well documented, as the "reducer" can in fact get called several times, basically "reducing again" output that has already been through reduce processing.
This is generally how mapReduce deals with "large inputs", where there are lots of values for a given "key" and the "reducer" only processes so many of them at one time. For example a reducer may actually only take 30 or so documents emitted with the same key, reduce two sets of those 30 down to 2 documents and then finally reduce to a single output for a single key.
The end result here is the same as the other output shown above, with the mapReduce difference that everything is under a "value" key as that is just how it works.
So a couple of ways to do it depending on your data. Do try to stick with the aggregation framework where possible as it is much faster and modern versions can consume and output just as much data as you can throw at mapReduce.