Function result not accessible outside of scope - javascript

I have a function that works and returns data correctly to a console.log. How do I then wrap this function up and call on it, retrieving the data as needed? I have tried the below with no luck.
All this code works:
function weekendPlans() {
Entry.aggregate(
[
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 1 ] },
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 6 ] },
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 7 ] }
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
],
// GET THE RESULTS AND RETURN IF selectedDate MATCHES THIS WEEKEND
function(err,results) {
if (err) throw err;
//console.log(results);
var i = results.length;
var theWeekend;
while(i--) {
if(results[i].selectedDate === friday || saturday || sunday) {
theWeekend = results[i];
break;
}
}
console.log(theWeekend);
}
)};
Calling the function outside of scope returns undefined
console.log(weekendPlans());
Anticipated result:
{ _id: 56fe9fe71f84acc2564b9fe8,
url: 'http://www.timeoutshanghai.com/features/Blog-Food__Drink/35271/Baristas-showcase-latte-art-in-Shanghai.html',
title: 'TIMEOUT',
selectedDate: Sat Apr 02 2016 01:00:00 GMT+0100 (BST),
__v: 0 }

Because this is an asynchronous operation, you'll need to rethink the way you implement functions. Borrowing from Node's event-driven model, add a callback to it:
function weekendPlans(callback) {
// ^ this is the magic param
Entry.aggregate(
[
{ "$redact": {
"$cond": {
"if": {
"$or": [
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 1 ] },
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 6 ] },
{ "$eq": [ { "$dayOfWeek": "$selectedDate" }, 7 ] }
]
},
"then": "$$KEEP",
"else": "$$PRUNE"
}
}}
],
// GET THE RESULTS AND RETURN IF selectedDate MATCHES THIS WEEKEND
function(err,results) {
// if (err) throw err;
// we don't need to throw an error here, we'll pass it in the cb function
//console.log(results);
var i = results.length;
var theWeekend;
while(i--) {
if(results[i].selectedDate === friday || saturday || sunday) {
theWeekend = results[i];
break;
}
}
callback(err, theWeekend)
// ^ call the callback
}
)};
Then use it like so:
weekendPlans(function(err, theWeekend) {
if (err) throw err
// now you can check for err and reference theWeekend
})

Related

MongoDB Aggregate Match - check if value contains any of the given strings

I have the following types of documents in my mongodb. How can i use a match function to check if the key2 value contains 'Mermaid / Fairy' or 'Superhero'?
{
_id: 123,
key2: [ 'Mermaid / Fairy', 'Superhero' ]
}
{
_id: 456,
key2: [ 'Slug']
}
This is how i am doing matches for individual words, however i would like to pass in a couple, and if it matches any of them, then it gets returned
{
$match: { key2: /.*Superhero.*/ },
},
you can use this aggregate
itemsSchema.aggregate([
{
$match: {
"key2": {
$in: [
"Mermaid / Fairy",
"Superhero"
]
}
}
}])
Here are a couple of ways ...
to check if the key2 value contains 'Mermaid / Fairy' or 'Superhero'
... by checking if the "$size" of the "$setIntersection" of "$key2" and ["Mermaid / Fairy", "Superhero"]
db.collection.aggregate([
{
"$match": {
"$expr": {
"$gt": [
{
"$size": {
"$setIntersection": [
"$key2",
["Mermaid / Fairy", "Superhero"]
]
}
},
0
]
}
}
}
])
Try it on mongoplayground.net.
Another way is to use "$reduce" by checking each "$key2" value to see if it is "$in" ["Mermaid / Fairy", "Superhero"].
db.collection.aggregate([
{
"$match": {
"$expr": {
"$reduce": {
"input": "$key2",
"initialValue": false,
"in": {
"$or": [
"$$value",
{
"$in": [
"$$this",
["Mermaid / Fairy", "Superhero"]
]
}
]
}
}
}
}
}
])
Try it on mongoplayground.net.

Javascript - reduce the given object into one data structure

I need to reduce the given object into some datastructure. This is my input object.
const receiver = {
USER1: {
module: ['a_critical', 'a_normal','b_normal']
},
USER2: {
module: ['a_critical', 'a_normal','b_critical']
},
USER3: {
module: ['a_critical']
}
};
const allModules = ['a_normal', 'a_critical', 'b_normal', 'b_critical'];
Desired output:
{
"a_critical": [
{
"user": [
"USER1", "USER2", "USER3"
]
}
],
"a_normal": [
{
"user": [
"USER1", "USER2"
]
}
],
"b_normal": [
{
"user": [
"USER1"
]
}
],
"b_critical": [
{
"user": [
"USER2"
]
}
]
}
I have tried doing but i was getting some problems. I am getting some duplicate properties which should be there. I can share the code on what i have tried.
const receiverObj = {};
let count = 0;
Object.keys(receiver).forEach((item) => {
receiver[item].module.forEach((module) => {
if(allModules.includes(module)) {
count = 1;
if(count) {
receiverObj[module] = [];
receiverObj[module].push({user: [item] });
}
receiverObj[module].push({user: item });
count = 0;
}
})
});
console.log(JSON.stringify(receiverObj, null, 2));
Actual result which i got:
{
"a_critical": [
{
"user": [
"USER3"
]
},
{
"user": "USER3"
}
],
"a_normal": [
{
"user": [
"USER2"
]
},
{
"user": "USER2"
}
],
"b_normal": [
{
"user": [
"USER1"
]
},
{
"user": "USER1"
}
],
"b_critical": [
{
"user": [
"USER2"
]
},
{
"user": "USER2"
}
]
}
Is there any optimal way of doing this ? can someone help ?
Iterate over each module in a reduce callback, creating a { user: [] } object in the accumulator if it doesn't exist yet, and then push to that array:
const receiver = {
USER1: {
module: ['a_critical', 'a_normal','b_normal']
},
USER2: {
module: ['a_critical', 'a_normal','b_critical']
},
USER3: {
module: ['a_critical']
}
};
const output = Object.entries(receiver)
.reduce((a, [user, { module }]) => {
module.forEach((name) => {
if (!a[name]) {
a[name] = { user: [] };
}
a[name].user.push(user);
});
return a;
}, {});
console.log(output);
You could also create the accumulator object in advance, if you wanted, since you have allModules, thereby avoiding conditionals inside the .reduce:
const receiver = {
USER1: {
module: ['a_critical', 'a_normal','b_normal']
},
USER2: {
module: ['a_critical', 'a_normal','b_critical']
},
USER3: {
module: ['a_critical']
}
};
const allModules = ['a_normal', 'a_critical', 'b_normal', 'b_critical'];
const accum = Object.fromEntries(
allModules.map(
name => [name, { user: [] }]
)
);
const output = Object.entries(receiver)
.reduce((a, [user, { module }]) => {
module.forEach((name) => {
a[name].user.push(user);
});
return a;
}, accum);
console.log(output);
Use Array.prototype.reduce()
const receiver = {
USER1: {
module: ['a_critical', 'a_normal','b_normal']
},
USER2: {
module: ['a_critical', 'a_normal','b_critical']
},
USER3: {
module: ['a_critical']
}
}
const allModules = ['a_normal', 'a_critical', 'b_normal', 'b_critical']
const result = allModules.reduce((modulesObj, moduleName) => {
modulesObj[moduleName] = [{ user: [] }]
for (let user in receiver) {
if (receiver[user].module.includes(moduleName)) {
modulesObj[moduleName][0].user.push(user)
}
}
return modulesObj
}, {})
console.log(result)
Try this one
const receiver = {
USER1: {
module: ['a_critical', 'a_normal','b_normal']
},
USER2: {
module: ['a_critical', 'a_normal','b_critical']
},
USER3: {
module: ['a_critical']
}
};
const userByModules = Object.keys(receiver).reduce(function (acc, user) {
receiver[user].module.forEach((module) => {
if (acc[module]) {
acc[module].user.push(user);
} else {
acc[module] = {user: [user]};
}
});
return acc;
}, {});
console.log(userByModules);
const receiver = {
USER1: {
module: ["a_critical", "a_normal", "b_normal"]
},
USER2: {
module: ["a_critical", "a_normal", "b_critical"]
},
USER3: {
module: ["a_critical"]
}
};
const allModules = ["a_normal", "a_critical", "b_normal", "b_critical"];
let reduce = () => {
// create output object
let output = {};
// add modules into output
allModules.map(mod => {
return (output[mod] = [{ 'user': []}]);
});
// map users to modules
for (let userKey in receiver) {
let userModules = receiver[userKey].module;
userModules.forEach(mod => {
if(output.hasOwnProperty(mod)) {
output[mod][0]['user'].push(userKey);
}
});
}
};
reduce();
Straightforward way of getting the job done. No fancy functions used.Hopefully this logic would be easy to follow.
The below snippet reduce the provided allModules array into the expected result. However, I think this structure
"a_normal": {
"user": [
"USER1",
"USER2"
]
},
...
make sense more than the below snippet because you have to access the first index of each module to get user value
"a_normal": [
{
"user": [
"USER1",
"USER2"
]
}
],
const receiver = {
USER1: { module: ["a_critical", "a_normal", "b_normal"] },
USER2: { module: ["a_critical", "a_normal", "b_critical"] },
USER3: { module: ["a_critical"] }
};
const allModules = ["a_normal", "a_critical", "b_normal", "b_critical"];
const result = allModules.reduce((prevResult, curModule, index) => {
if(!prevResult[curModule]) {
prevResult[curModule]=[{user:[]}];
}
Object.keys(receiver).forEach((user) => {
if(receiver[user].module.includes(curModule) &&
!prevResult[curModule][0].user.includes(user)) {
prevResult[curModule][0].user.push(user);
}
});
return prevResult;
},{});
console.log(JSON.stringify(result, null, 2));

Building your MongoDB $match as a string dynamically

Im trying to build my $match dynamically for my MongoDB request, and when I do the simple stuff it works perfect, like this:
var matchStr = {};
matchStr.status = { "$lte": 3 };
matchStr.checkout = { $gte: Math.round(new Date(d).getTime()/1000) }
And then I run the
bookingTable.aggregate([
{
$match: {
$and: [ matchStr ]
}
}, etc....
Which gives a nice:
matchStr: {
"status": {
"$lte": 3
},
"checkout": {
"$gte": 1527669588
}
}
So thats all great, but what if I want to put something like this into the matchStr...
{ $or: [ { "managerDate": {$lte: managerLast} }, { "activityDate": {$lte: activityLast} } ] }
,
{ $or: [ { "expireDate": {$gt: oneDayBackward} }, { "status": {$lt: 9}} ] }
,
{ "status": { $in: [0, 1, 2, 9 ] } }
How can I do that?
There are multiple syntax for accessing the property of an object
var matchStr = {}
matchStr.status = { "$lte": 3 }
matchStr.checkout = { "$gte": Math.round(new Date().getTime()/1000) }
matchStr["$or"] = [
{ "managerDate": { "$lte": "managerLast" }},
{ "activityDate": { "$lte": "activityLast" }}
]
or If you want to push to $or operator
matchStr["$or"].push({ "managerDate": { "$lte": "managerLast" } })
matchStr["$or"].push({ "activityDate": { "$lte": "activityLast" } })

How to find the matched element using custom ranking algorithm

I need to calculate rank on each account. Rank depends on the account who requested the method.
var account; // assumed to be one requesting the method
const allMatches = Account.find({
$or: [
{ argA: account.argA },
{ argB: account.argB },
{ argC: account.argC }
],
_id: {$ne: account._id}
});
const getRank = (item) => {
let rank = 0;
if (item.argB === account.argB) {
rank += 3;
}
if (item.argA === account.argA) {
rank += 2;
}
if (item.argC === account.argC) {
rank += 1;
}
return rank;
};
const compare = (a,b) => {
const rankA = getRank(a);
const rankB = getRank(b);
if (rankA === rankB) return 0;
return rankA > rankB ? 1: -1;
}
// use the compare method
allMatches.sort(compare);
account.set({match : allMatches[0]['id']});
However, I cant use the sort method like this, as it expects an object or a string.
I need some help to proceed in the correct direction.
For example -
If there are 3 accounts in the system
A1 - {argA: 'A', argB: 'B', argC: 'C'}
A2 - {argA: 'D', argB: 'B', argC: 'F'}
A3 - {argA: 'G', argB: 'H', argC: 'C'}
Now if A1 needs to find a match -
Rank score with A2 is = 3 // argB is same
Rank score with A3 is = 1 // argC is same
Hence A1 will match with A2, and thats what I need.
You can "sort on the database" using .aggregate()
let results = await Account.aggregate([
{ "$match": {
"$or": [
{ "argA": account.argA },
{ "argB": account.argB },
{ "argC": account.argC }
],
"_id": { "$ne": account._id}
}},
{ "$addFields": {
"rank": {
"$sum": [
{ "$cond": [{ "$eq": ["$argA", account.argA] }, 3, 0 ] },
{ "$cond": [{ "$eq": ["$argB", account.argB] }, 2, 0 ] },
{ "$cond": [{ "$eq": ["$argC", account.argC] }, 1, 0 ] }
]
}
}},
{ "$sort": { "rank": -1 } }
/*
* Add $skip and $limit for paging if needed
{ "$skip": 0 },
{ "$limit": 25 },
*/
])
Also noting that Account.aggregate() or Account.find() actually returns a Promise since the method is async, so you need to await that or use .then() or provide a callback, depending on your preferred style:
Account.aggregate([
{ "$match": {
"$or": [
{ "argA": account.argA },
{ "argB": account.argB },
{ "argC": account.argC }
],
"_id": { "$ne": account._id}
}},
{ "$addFields": {
"rank": {
"$sum": [
{ "$cond": [{ "$eq": ["$argA", account.argA] }, 3, 0 ] },
{ "$cond": [{ "$eq": ["$argB", account.argB] }, 2, 0 ] },
{ "$cond": [{ "$eq": ["$argC", account.argC] }, 1, 0 ] }
]
}
}},
{ "$sort": { "rank": -1 } }
/*
* Add $skip and $limit for paging if needed
{ "$skip": 0 },
{ "$limit": 25 },
*/
]).then( result => /* do something */ )
That promise/callback resolution is the basic error in your code.
But the general point is you probably want to calculate "rank" on the server in order to enable paging over larger result sets, and that's a lot smarter than trying to sort "everything" in a result array.

skipped count 0 in aggregate function

I'm stuck on this for couple of days. I'm trying to get the count: 0 where there is no documents in the given time period. This is the aggregate function I'm using at the moment:
var getCount = function(timeBlock, start, end, cb) {
Document.aggregate(
{
$match: {
time: {
$gte: new Date(start),
$lt: new Date(end)
}
}
},
{
$project: {
time: 1,
delta: { $subtract: [
new Date(end),
'$time'
]}
}
},
{
$project: {
time: 1,
delta: { $subtract: [
"$delta",
{ $mod: [
"$delta",
timeBlock
]}
]}
}
},
{
$group: {
_id: { $subtract: [
end,
"$delta"
]},
count: { $sum: 1 }
}
},
{
$project: {
time: "$_id",
count: 1,
_id: 0
}
},
{
$sort: {
time: 1
}
}, function(err, results) {
if (err) {
cb(err)
} else {
cb(null, results)
}
})
}
I tried using $cond, but with no luck
The group stage is producing documents based on grouping on your given _id and counting the number of documents from the previous stage that end up in the group. Hence, a count of zero would be the result of a document being created from 0 input documents belonging to the group. Thinking about it this way, it's clear that there's no way the aggregation pipeline can do this for you. It doesn't know what all of the "missing" time periods are and it can't invent the appropriate documents out of thin air. Reapplying your extra knowledge about the missing time periods to complete the picture at the end seems like a reasonable solution (not "hacky") if you need to have an explicit count of 0 for empty time periods.
Though it has already been said the best thing to do here is "merge" your results post process rather than expect "keys" that do not exist to appear or to issue multiple queries with explicit keys that are possibly not going to aggregate results and combine them.
What has not already been said is how you actually do this, so I'll give you a MongoDB "thinking" kind of way to collect your results.
As a quick disclaimer, you could possibly employ much the same approach by "seeding" empty keys for each interval using mapReduce, or possibly even altering your data so that there is always an empty value within each possible block. Those approaches seem basically "hacky" and in the mapReduce case is not going to provide the best performance or muliple results.
What I would suggest is that working with collection results for the MongoDB brain can be made simple. There is a neat little solution called neDB, which is billed as a kind of SQL Lite for MongoDB. It supports a subset of functionality and is therefore perfect for "in memory" manipulation of results with a MongoDB mindset:
var async = require('async'),
mongoose = require('mongoose'),
DataStore = require('nedb'),
Schema = mongoose.Schema;
var documentSchema = new Schema({
time: { type: Date, default: Date.now }
});
var Document = mongoose.model( "Document", documentSchema );
mongoose.connect('mongodb://localhost/test');
var getCount = function(timeBlock, start, end, callback) {
async.waterfall(
[
// Fill a blank series
function(callback) {
var db = new DataStore();
var current = start;
async.whilst(
function() { return current < end },
function(callback) {
var delta = end - current;
db.insert({ "_id": end - delta, "count": 0 },function(err,doc) {
//console.log( doc );
current += timeBlock;
callback(err);
});
},
function(err) {
callback(err,db);
}
);
},
// Get data and update
function(db,callback) {
var cursor = Document.collection.aggregate(
[
// Match documents
{ "$match": {
"time": {
"$gte": new Date(start),
"$lt": new Date(end)
}
}},
// Group. 1 step and less hacky
{ "$group": {
"_id": {
"$let": {
"vars": {
"delta": {
"$subtract": [
{ "$subtract": [ new Date(end), "$time" ] },
{ "$mod": [
{ "$subtract": [ new Date(end), "$time" ] },
timeBlock
]}
]
}
},
"in": { "$subtract": [ end, "$$delta" ] }
}
},
"count": { "$sum": 1 }
}}
],
{ "cursor": { "batchSize": 100 } }
);
cursor.on("data",function(item) {
cursor.pause();
console.log( "called" );
db.update(
{ "_id": item._id },
{ "$inc": { "count": item.count } },
{ "upsert": true },
function(err) {
cursor.resume();
}
);
});
cursor.on("end",function() {
console.log( "done" );
db.find({},function(err,result) {
callback(err,result);
});
});
}
],
function(err,result) {
callback(err,result);
}
);
}
mongoose.connection.on("open", function(err,conn) {
getCount(
1000 * 60 * 60, // each hour
new Date("2014-07-01").valueOf(), // start
new Date("2014-07-02").valueOf(), // end
function(err,result) {
if (err) throw err;
console.log( result );
}
);
});
So essentially create each interval as in memory collection and then just update those interval records with the actual data retrieved. I can't think of another way to do that where it would be more simple and natural to the way of thinking.
Just a footnote, the "interval" logic is just replicated from your question, but in fact the time periods are "rounded up" where 15 minutes would appear in hour 1. It usually is the practice to round down so that everything belongs to the interval it falls in and not the next one.
this is hacky fix I did for now:
var getCount = function(timeBlock, start, end, cb) {
Document.aggregate(
{
$match: {
time: {
$gte: new Date(start),
$lt: new Date(end)
}
}
},
{
$project: {
time: 1,
delta: { $subtract: [
new Date(end),
'$time'
]}
}
},
{
$project: {
time: 1,
delta: { $subtract: [
"$delta",
{ $mod: [
"$delta",
timeBlock
]}
]}
}
},
{
$group: {
_id: { $subtract: [
end,
"$delta"
]},
count: { $sum: 1 }
}
},
{
$project: {
time: "$_id",
count: 1,
_id: 0
}
},
{
$sort: {
time: 1
}
}, function(err, results) {
if (err) {
cb(err)
} else {
// really hacky way
var numOfTimeBlocks = ( end - start ) / timeBlock
// in case there is no 0s in the given period of time there is no need
// to iterate through all of the results
if ( results.length === numOfTimeBlocks ) {
cb(results);
} else {
var time = start;
var details = [];
var times = results.map(function(item) {
return item.time;
});
for( var i = 0; i < numOfTimeBlocks; i++) {
time += timeBlock;
var idx = times.indexOf(time);
if (idx > -1) {
details.push(results[idx]);
} else {
var documentCount = { count: 0, time: time };
details.push(documentCount);
}
}
cb(details);
}
}
})
}
I was also thinking about doing one query per time block, which gives the same result but I think is inefficient because you query the database N times.

Categories