MapReduce sub-document - mongodb

I am trying to graph email activity that I am recording in a Mongo db. Whenever I send out an email I create a record, then, when there is activity on the email (open, click, mark as spam) I update the document by adding to it's history.
Here is a sample document:
{
"_id" : new BinData(3, "wbbS0lRI0ESx5DyStKq9pA=="),
"MemberId" : null,
"NewsletterId" : 4,
"NewsletterTypeId" : null,
"Contents" : "[message goes here]",
"History" : [{
"EmailActionType" : "spam",
"DateAdded" : new Date("Sat, 10 Dec 2011 04:17:26 GMT -08:00")
}, {
"EmailActionType" : "processed",
"DateAdded" : new Date("Sun, 11 Dec 2011 04:17:26 GMT -08:00")
}, {
"EmailActionType" : "deffered",
"DateAdded" : new Date("Mon, 12 Dec 2011 04:17:26 GMT -08:00")
}],
"DateAdded" : new Date("Mon, 01 Jan 0001 00:00:00 GMT -08:00")
}
What I would like to do is query the database for a specific history date range. The end result should be a list with an item for each day where there is an activity and a total for each activity type:
date: "20111210", spam: 1, processed: 0, deffered: 0
date: "20111211", spam: 0, processed: 1, deffered: 0
date: "20111212", spam: 0, processed: 0, deffered: 1
Here is what I currently have:
db.runCommand({ mapreduce: Email,
map : function Map() {
var key = this.NewsletterId;
emit(
key,
{ "history" : this.History }
);
}
reduce : function Reduce(key, history) {
var from = new Date (2011, 1, 1, 0, 0, 0, 0);
var to = new Date (2013, 05, 15, 23, 59, 59, 0);
// \/ determine # days in the date range \/
var ONE_DAY = 1000 * 60 * 60 * 24; // The number of milliseconds in one day
var from_ms = from.getTime(); // Convert both date1 to milliseconds
var to_ms = to.getTime(); // Convert both date1 to milliseconds
var difference_ms = Math.abs(from_ms - to_ms); // Calculate the difference in milliseconds
var numDays = Math.round(difference_ms/ONE_DAY); // Convert back to days and return
// /\ determine # days between the two days /\
var results = new Array(numDays); //array where we will store the results. We will have an entry for each day in the date range.
//initialize array that will contain our results for each type of emailActivity
for(var i=0; i < numDays; i++){
results[i] = {
numSpam: 0,
numProcessed: 0,
numDeffered: 0
}
}
//traverse the history records and count each type of event
for (var i = 0; i < history.length; i++){
var to_ms2 = history[i].DateAdded.getTime(); // Convert both date1 to milliseconds
var difference_ms2 = Math.abs(from_ms - to_ms2); // Calculate the difference in milliseconds
var resultsIndex = Math.round(difference_ms2/ONE_DAY); //determine which row in the results array this date corresponds to
switch(history[i].EmailActionType)
{
case 'spam':
results[resultsIndex].numSpam = ++results[resultsIndex].numSpam;
break;
case 'processed':
results[resultsIndex].numProcessed = ++results[resultsIndex].numProcessed;
break;
case 'deffered':
results[resultsIndex].numDeffered = ++results[resultsIndex].numDeffered;
break;
}
}
return results;
}
finalize : function Finalize(key, reduced) {
return {
"numSpam": reduced.numSpam,
"numProcessed": reduced.numProcessed,
"numDeffered": reduced.numDeffered,
};
}
out : { inline : 1 }
});
When I run it, I don't get anything, but I'm also not getting any errors, so not really sure where to look.

Your problem is definitely in your Map / Reduce functions. There is a disconnect between your emit and your expected output.
Your expected output:
date: "20111210", spam: 1, processed: 0, deffered: 0
Map / Reduce always outputs in terms of a key and a value. So your output would look like this:
_id: "20111220", value: { spam: 1, processed: 0, deferred: 0 }
Here is the basic premise. Your emit needs to output data of the correct format. So if you emit(key, value), then you should have:
var key='20111220'
var value={spam:1, processed:0, deferred:0}
In your case, you are emitting several times per document as you loop through History. This is normal.
The reduce function is only run if there are multiple values for the same key. So if you have this:
_id: "20111220", value: { spam: 1, processed: 0, deferred: 0 }
_id: "20111220", value: { spam: 1, processed: 2, deferred: 0 }
Then reduce will pull those together and give you this:
_id: "20111220", value: { spam: **2**, processed: **2**, deferred: 0 }
Here is a quick stab at the answer:
map = function() {
for(var i in this.History) {
var key = get_date(this.History[i].DateAdded);
var value = {spam: 0, processed: 0, deffered: 0};
if(this.History[i].EmailActionType == "Spam") { value.spam++; }
else if(....)
...
emit(key, value);
}
}
reduce = function(key, values) {
// values is an array of these things {spam: 0, processed: 0, deffered: 0}
var returnValue = { spam: 1, processed: 0, deffered: 0 };
for(var i in values) {
returnValue.spam += values[i].spam;
returnValue.processed += values[i].processed;
returnValue.deffered += values[i].deffered;
}
return returnValue;
}
Just remember that the structure of emit has to match the structure of your final values.

Related

How can I define Date in my defined mongoose Schema such as to return a nicely formated date and time?

So I want to create a task/todo model for my app and for every task I want to have a nice looking date and time of creation in the footer. I have already achieved this successfuly on a similar client side only app,:
function getCurrentDateTime () {
const date = new Date();
let day = date.getDate().toString().length <= 1 ? '0' + date.getDate() : date.getDate();
let month = date.getMonth().toString().length <= 1 ? `0${parseInt(date.getMonth() + 1)}` : date.getMonth();
let year = date.getFullYear().toString().length <= 1 ? '0' + date.getFullYear() : date.getFullYear();
let hours = date.getHours().toString().length <= 1 ? '0' + date.getHours() : date.getHours();
let minutes = date.getMinutes().toString().length <= 1 ? '0' + date.getMinutes() : date.getMinutes();
let seconds = date.getSeconds().toString().length <= 1 ? '0' + date.getSeconds() : date.getSeconds();
return { day, month, year, hours, minutes, seconds };
}
function createTask (statePlaceholder, currentTaskText) {
let newTask = {
id: uuid(),
text: currentTaskText,
completed: false,
creationDateTime: {
date: `${getCurrentDateTime().day}/${getCurrentDateTime().month}/${getCurrentDateTime().year}`,
time: `${getCurrentDateTime().hours}:${getCurrentDateTime().minutes}:${getCurrentDateTime().seconds}`
}
};
...
}
and it looks like this:
I want to save all the tasks elements(text, completed or not and date/time of creation) on MongoDB, and I don't know how to define the date and time so that I get what you see in the image, but comming from MongoDB.
const TaskSchema = new mongoose.Schema({
text: { type: String, required: true },
completed: Boolean,
creationDateTime: {
date: // day/month/year,
time: // timestamp
}
});
How can I properly define the date and time on the mongoose schema defined by me?
What you're trying to store as creationDateTime should definitely be of type Date and you should not store it as string or object. It will make any future querying easier and will let you avoid some unexpected issues which may happen if you stored this value as string. You can take advantage of mongoose's default feature which will run Date.now any time you store a new document so your schema can look like this:
const TaskSchema = new mongoose.Schema({
text: { type: String, required: true },
completed: Boolean,
creationDateTime: {
type: Date,
default: Date.now
}
});
When it comes to formatting, mongoose offers a nice capability of defining virtual properties. Such field will not be stored in a database but will by dynamically evaluated and this is where you can reuse your formatting logic:
function dateTimeToParts(date) {
let day = date.getDate().toString().length <= 1 ? '0' + date.getDate() : date.getDate();
let month = date.getMonth().toString().length <= 1 ? `0${parseInt(date.getMonth() + 1)}`: date.getMonth();
let year = date.getFullYear().toString().length <= 1 ? '0' + date.getFullYear() : date.getFullYear();
let hours = date.getHours().toString().length <= 1 ? '0' + date.getHours() : date.getHours();
let minutes = date.getMinutes().toString().length <= 1 ? '0' + date.getMinutes() : date.getMinutes();
let seconds = date.getSeconds().toString().length <= 1 ? '0' + date.getSeconds() : date.getSeconds();
return { day, month, year, hours, minutes, seconds };
}
TaskSchema.virtual('createdOn').get(function() {
let { day, month, year, hours, minutes, seconds } = dateTimeToParts(
this.creationDateTime
);
return {
date: `${day}/${month}/${year}`,
time: `${hours}:${minutes}:${seconds}`
};
});
So having below document in your MongoDB database:
{
"_id" : ObjectId("5e2e7c93397e8124b81dfcaa"),
"creationDateTime" : ISODate("2020-01-27T06:00:51.409Z"),
"text" : "abc",
"__v" : 0
}
You can run following code:
let task = await Task.findOne({ _id: ObjectId("5e2e7c93397e8124b81dfcaa") });
console.log(task.createdOn);
To get following output:
{ date: '26/01/2020', time: '22:00:51' }

how we can get all document that have period time more than some min

Let assume we have 3 documents. The first one is created 6:00, the second one at 6:25 and the third one at 7:00. How can I write a query to get all documents after 5:45 that the period of time between them is less than 30 min? In current example i want get first and second because the third one is more than 30 min from the last one.
Save period in number format.
collection.find({ period : { $gt : 5:45, $lt : 30}})
var arr = db.times.find( { time: { $gte: ISODate("2019-12-30T04:00:00.000+00:00") } } ).sort( { time: 1 } ).toArray();
const THIRTY_MINS = 30 * 60 * 1000;
var prevTime = arr[0].time;
var set = new Set();
for ( let i = 1; i < arr.length; i++ ) {
if ( ( arr[i].time - prevTime ) <= THIRTY_MINS ) {
set.add( arr[i - 1] );
set.add( arr[i] );
}
prevTime = arr[i].time;
}
var resultArr = [...set];
Verify with input documents:
{ time: ISODate("2019-12-30T06:00:00.000+00:00") },
{ time: ISODate("2019-12-30T06:25:00.000+00:00") },
{ time: ISODate("2019-12-30T07:00:00.000+00:00") }

How to find maximum time from array?

I have an array. I want to find the maximum time from array.
I tried the follwing code.
var maxtm = Math.max.apply(null, this.my_arr.map(function (e) {
return e['time'];
}));
this.my_arr = [{
date: '21-jun-2019',
time: '21:22:00'
}, {
date: '21-june-2019',
time: '11:33:23',
}, {
date: '21-june-2019',
time: '12:12:00'
}]
I expect the output '21:22:00', but the actual output is NaN.
Try convert to int parseInt(time.split(':').join(''), 10)
Example
var maxtm = Math.max.apply(null, this.my_arr.map(function (e) { return parseInt(e['time'].split(':').join(''), 10);}));
After you need convert max back to time

Can't advance past endIndex Swift

Something wrong with String Index when checking and deleting symbols. How can I improve it?
func romanToInt(_ s: String) -> Int {
let romanDigits = ["I" : 1,
"V" : 5,
"X" : 10,
"L" : 50,
"C" : 100,
"D" : 500,
"M" : 1000]
let romanSums = ["IV" : 4,
"IX" : 9,
"XL" : 40,
"XC" : 90,
"CD" : 400,
"CM" : 900]
var sum = 0
var str = s
var charIndex = str.startIndex
for index in str.indices {
if index != str.index(before: str.endIndex) {
charIndex = str.index(after: index)
} else {
charIndex = str.index(before: str.endIndex)
}
let chars = String(str[index]) + String(str[charIndex])
if romanSums[chars] != nil {
print(chars)
str.remove(at: charIndex)
sum += romanSums[chars]!
print(sum)
} else {
let char = String(str[index])
print(char)
sum += romanDigits[char]!
print(sum)
}
print(str)
}
return sum
}
let check = romanToInt("MCMXCIV")
CONSOLE LOG:
M
1000
MCMXCIV
CM
1900
MCXCIV
XC
1990
MCXIV
IV
1994
MCXI
Fatal error: Can't advance past endIndex
You are modifying the string you are iterating over, so your indices become invalid. Instead, you could add a skipChar boolean that says that you've already handled the next character and then skip that character by executing continue:
func romanToInt(_ s: String) -> Int {
let romanDigits = ["I" : 1,
"V" : 5,
"X" : 10,
"L" : 50,
"C" : 100,
"D" : 500,
"M" : 1000]
let romanSums = ["IV" : 4,
"IX" : 9,
"XL" : 40,
"XC" : 90,
"CD" : 400,
"CM" : 900]
var sum = 0
var str = s
var charIndex = str.startIndex
var skipChar = false
for index in str.indices {
if skipChar {
skipChar = false
continue
}
if index != str.index(before: str.endIndex) {
charIndex = str.index(after: index)
} else {
charIndex = str.index(before: str.endIndex)
}
let chars = String(str[index]) + String(str[charIndex])
if romanSums[chars] != nil {
print(chars)
skipChar = true
sum += romanSums[chars]!
print(sum)
} else {
let char = String(str[index])
print(char)
sum += romanDigits[char]!
print(sum)
}
print(str)
}
return sum
}
let check = romanToInt("MCMXCIV")
print(check)
1994
for index in str.indices {
...
str.remove(at: charIndex)
It is not valid to modify a string while you are iterating over it. str.indices is fetched one time here, and is no longer valid once you've modified the underlying string.
I'm sure there will be a lot of implementations of this because it's the kind of small, fun problem that attracts implementations. So why not? This just screams recursion to me.
let romanDigits: [Substring: Int] = ["I" : 1,
"V" : 5,
"X" : 10,
"L" : 50,
"C" : 100,
"D" : 500,
"M" : 1000]
let romanSums: [Substring: Int] = ["IV" : 4,
"IX" : 9,
"XL" : 40,
"XC" : 90,
"CD" : 400,
"CM" : 900]
func romanToInt<S: StringProtocol>(_ s: S) -> Int
where S.SubSequence == Substring {
if s.isEmpty { return 0 }
if let value = romanSums[s.prefix(2)] {
return value + romanToInt(s.dropFirst(2))
} else if let value = romanDigits[s.prefix(1)] {
return value + romanToInt(s.dropFirst(1))
} else {
fatalError("Invalid string")
}
}
let check = romanToInt("MCMXCIV")
Of course this doesn't really check for valid sequences, so it's kind of junk. "IIIIXXIII" is kind of gibberish, but it works. But it's in keeping with the original approach.
Use reduce to make it flow here:
func romanToInt(_ s: String) -> Int {
if s.isEmpty {return 0}
let romanDigits = ["I" : 1,
"V" : 5,
"X" : 10,
"L" : 50,
"C" : 100,
"D" : 500,
"M" : 1000]
let romanSums = ["IV" : 4,
"IX" : 9,
"XL" : 40,
"XC" : 90,
"CD" : 400,
"CM" : 900]
return s.dropFirst().reduce((s.first!, romanDigits["\(s.first!)"]!)){
return ( $1, //current char
$0.1 + //previous sum
(romanSums["\($0.0)\($1)"] //add double value
?? ((romanDigits["\($1)"]!). + romanDigits["\($0.0)"]!)) //or single value and add duplicated
- romanDigits["\($0.0)"]!) // minus duplicated
}.1
}
print(romanToInt("MCMXCIV")). //1994
You are mutating str inside the loop, its end index is going change, in this case it gets lower than its original value. You can fix your code by checking that you haven't exceeded the endIndex on each iteration by using a while loop :
var index = str.startIndex
while index < str.endIndex {
...
//Before the closing curly brace of the while loop
index = str.index(after: index)
}
I was trying to reproduce a crash reported by one my users with the same message Can't advance past endIndex but I was not able to do it. Your code helped me figure out that this error changed in later versions of swift.
Your same code would report cannot increment beyond endIndex with swift 4.x runtime libraries, and String index is out of bounds with 5.x. The exact version numbers for the changes I do not know. But I suspect it is 4.0.0 and 5.0.0.-

map invoke failed: JS Error: Error: fast_emit takes 2 args (anon):1

A collection will be created from the below js file , i need three Fields in my collection so i have given
emit(this.cust_id, 1,date);
Its giving the below Error
map invoke failed: JS Error: Error: fast_emit takes 2 args (anon):1
But its working fine with emit(this.cust_id, 1);
Please tell me ow to include date aslo in the collection created ??
m = function() {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate()-1);
var month = (currentDate.getMonth() < 9 ? "0"+ (currentDate.getMonth()+1) : (currentDate.getMonth()+1));
var day = (currentDate.getDate() < 10 ? "0" + currentDate.getDate() : currentDate.getDate());
var date = currentDate.getTime();
emit(this.cust_id, 1,date);
}
r = function (k, vals) { var sum = 0; for (var i in vals) { sum += vals[i]; } return sum; }
q = function() {
var currentDate = new Date();
currentDate.setDate(currentDate.getDate()-1);
var month = (currentDate.getMonth() < 9 ? "0"+ (currentDate.getMonth()+1) : (currentDate.getMonth()+1));
var day = (currentDate.getDate() < 10 ? "0" + currentDate.getDate() : currentDate.getDate());
var date = currentDate.getTime();
var patt = date;
var query = {"created_at":"2013-30-04 11:19:52.587"};
return query;
}
res = db.logins.mapReduce(m, r, { query : q(), out : "LoginCount" });
As the error says, you can only emit two arguments. One represents the key over which you will be grouping/aggregating values and the other represents the value for this document.
If you need to calculate multiple fields you need to output a single value which is a document. In your example if 1 represents count and date represents some date you can output:
emit(this.cust_id, {count: 1, date: this.date);
This is if you are pulling the date from the document. I'm not sure why you would want to store the date when map was running, but obviously you can include your own date in that field.
Note that when you emit value as a document you must return the exact same format in your reduce function. You can see an example of that here, where they output two different values for each emit and then process both in reduce (you can ignore the finalize function).