JavaScript SUM and GROUP BY of JSON data - javascript

This is my first attempt at doing JavaScript with some JSON data objects and need some advice on the proper way to attain my goal.
Some server-side code actually generates a JSON formatted string that I have to work with and assign it to a string:
var dataString='$DATASTRING$';
But the end-result I have to work with after the server substitutes its data (without the \r\n, of course):
var dataString='[
{ "category" : "Search Engines", "hits" : 5, "bytes" : 50189 },
{ "category" : "Content Server", "hits" : 1, "bytes" : 17308 },
{ "category" : "Content Server", "hits" : 1, "bytes" : 47412 },
{ "category" : "Search Engines", "hits" : 1, "bytes" : 7601 },
{ "category" : "Business", "hits" : 1, "bytes" : 2847 },
{ "category" : "Content Server", "hits" : 1, "bytes" : 24210 },
{ "category" : "Internet Services", "hits" : 1, "bytes" : 3690 },
{ "category" : "Search Engines", "hits" : 6, "bytes" : 613036 },
{ "category" : "Search Engines", "hits" : 1, "bytes" : 2858 }
]';
And then I can change it to an object to work with.
var dataObject=eval("("+dataString+")");
This allows me to access the data individual rows of data, but I need to sum, group by, and order the values.
I need to the equivalent of an SQL statement like this:
SELECT category, sum(hits), sum(bytes)
FROM dataObject
GROUP BY category
ORDER BY sum(bytes) DESC
My desired output would be an object like this that I can further process:
var aggregatedObject='[
{ "category" : "Search Engines", "hits" : 13, "bytes" : 673684 },
{ "category" : "Content Server", "hits" : 3, "bytes" : 88930 },
{ "category" : "Internet Services", "hits" : 1, "bytes" : 3690 },
{ "category" : "Business", "hits" : 1, "bytes" : 2847 }
]';
...but i don't know where to start.
I could loop through all the category values and find the unique categories first, then loop again and sum the hits and bytes, then again to sort, but it seems there has got to be an easier way.
prototype.js (1.7) is already included on the client page, but I could add Underscore, jQuery, or some other small library if I had to.
I just don't know what would be best, easiest, smallest with the least amount of code to process the query.
Any suggestions?

You can use the native functions .reduce() to aggregrate the data, and then .sort() to sort by bytes.
var result = dataObject.reduce(function(res, obj) {
if (!(obj.category in res))
res.__array.push(res[obj.category] = obj);
else {
res[obj.category].hits += obj.hits;
res[obj.category].bytes += obj.bytes;
}
return res;
}, {__array:[]}).__array
.sort(function(a,b) { return b.bytes - a.bytes; });
If you're supporting older implementations, you'll need to use a shim for .reduce().

If you go the LINQ.js route, you can do it like this:
var aggregatedObject = Enumerable.From(dataArray)
.GroupBy("$.category", null,
function (key, g) {
return {
category: key,
hits: g.Sum("$.hits"),
bytes: g.Sum("$.bytes")
}
})
.ToArray();
Working demo with Stack Snippets:
var dataArray = [
{ category: "Search Engines", hits: 5, bytes: 50189 },
{ category: "Content Server", hits: 1, bytes: 17308 },
{ category: "Content Server", hits: 1, bytes: 47412 },
{ category: "Search Engines", hits: 1, bytes: 7601 },
{ category: "Business", hits: 1, bytes: 2847 },
{ category: "Content Server", hits: 1, bytes: 24210 },
{ category: "Internet ", hits: 1, bytes: 3690 },
{ category: "Search Engines", hits: 6, bytes: 613036 },
{ category: "Search Engines", hits: 1, bytes: 2858 }
];
var aggregatedObject = Enumerable.From(dataArray)
.GroupBy("$.category", null,
function (key, g) {
return {
category: key,
hits: g.Sum("$.hits"),
bytes: g.Sum("$.bytes")
}
})
.ToArray();
console.log(aggregatedObject);
<script src="//cdnjs.cloudflare.com/ajax/libs/linq.js/2.2.0.2/linq.min.js"></script>
Also, you can find more info on linqjs group by with a sum

Given the dataString above, the below code seems to work. It goes through each object; if the category exists in the groupedObjects array, its hits and bytes are added to the existing object. Otherwise, it is considered new and added to the groupedObjects array.
This solution makes use of underscore.js and jQuery
Here's a jsfiddle demo: http://jsfiddle.net/R3p4c/2/
var objects = $.parseJSON(dataString);
var categories = new Array();
var groupedObjects = new Array();
var i = 0;
_.each(objects,function(obj){
var existingObj;
if($.inArray(obj.category,categories) >= 0) {
existingObj = _.find(objects,function(o){return o.category === obj.category; });
existingObj.hits += obj.hits;
existingObj.bytes += obj.bytes;
} else {
groupedObjects[i] = obj;
categories[i] = obj.category;
i++;
}
});
groupedObjects = _.sortBy(groupedObjects,function(obj){ return obj.bytes; }).reverse();

var obj = [{Poz:'F1',Cap:10},{Poz:'F1',Cap:5},{Poz:'F1',Cap:5},{Poz:'F2',Cap:20},{Poz:'F1',Cap:5},{Poz:'F1',Cap:15},{Poz:'F2',Cap:5},{Poz:'F3',Cap:5},{Poz:'F4',Cap:5},{Poz:'F1',Cap:5}];
Array.prototype.sumUnic = function(name, sumName){
var returnArr = [];
var obj = this;
for(var x = 0; x<obj.length; x++){
if((function(source){
if(returnArr.length == 0){
return true;
}else{
for(var y = 0; y<returnArr.length; y++){
var isThere = [];
if(returnArr[y][name] == source[name]){
returnArr[y][sumName] = parseInt(returnArr[y][sumName]) + parseInt(source[sumName]);
return false;
}else{
isThere.push(source);
}
}
if(isThere.length>0)returnArr.push(source);
return false;
}
})(obj[x])){
returnArr.push(obj[x]);
}
}
return returnArr;
}
obj.sumUnic('Poz','Cap');
// return "[{"Poz":"F1","Cap":45},{"Poz":"F2","Cap":25},{"Poz":"F3","Cap":5},{"Poz":"F4","Cap":5}]"

Hi here is one solution written by me Visit: aggregate_groupby_js on npm
or in aggregate_groupby_js on github
The javascript library for using aggregate functions on array of objects. Basic functions like SUM, MIN, MAX, AVG, DISTINCT_COUNT for entire javascript objects
Example:
var arr = [{`"shape"`:`"square"`,`"color"`:`"red"`,`"used"`:1,`"instances"`:1},
{`"shape"`:`"square"`,`"color"`:`"red"`,`"used"`:2,`"instances"`:1},
{`"shape"`:`"circle"`,`"color"`:`"blue"`,`"used"`:0,`"instances"`:0},
{`"shape"`:`"square"`,`"color"`:`"blue"`,`"used"`:4,`"instances"`:4},
{`"shape"`:`"circle"`,`"color"`:`"red"`,"`used"`:1,`"instances"`:1},
{`"shape"`:`"circle"`,`"color"`:`"red"`,`"used"`:1,`"instances"`:0},
{`"shape"`:`"square"`,`"color"`:`"blue"`,`"used"`:4,`"instances"`:5},
{`"shape"`:`"square"`,`"color"`:`"red"`,`"used"`:2,`"instances"`:1}];
// Specify columns
var columns =[`"used"`, `"instances"`];
// Initialize object
var gb = new GroupBy(arr,columns);
// or
var gb = new GroupBy(arr,[`"used"`, `"instances"`]);
// Call the aggregate functions
gb.sum();
gb.min();
gb.max();
gb.avg();
gb.distinctCount();

Related

Can I make Mongo map reduce count multiple values of an object?

I have a collection that I am trying to map reduce by id and date to produce a graph for sales of a product in store vs online. A new object is created for each transaction, so I would like to reduce them to a total count for a given day. An object looks something like this:
object
{
"ProductID": 1
"Purchase Method": In Store
"Date": 2018-01-16
"Count": 5
}
What I am trying to achieve as output is to have in store and online purchases combined into 1 object with a key being the id and the date and then the value being the counts of each method as shown below:
ProductID: 1
Date: 2018-01-16
[
{Name: "In store", Count: 3}
{Name: "Online", Count: 2}
]
My current method was to map the objects by Id, date, and Purchase Method so the reduce would get the total count for that id on that date using that method, but this leads to having two entries for an id and date, 1 for in store and 1 for online. This is the current state of my functions:
var mapDailySales = function() {
var sale = this;
/*Converts timestamp to just date */
var pad = function pad(n, width, z) {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
};
var d = sale.Date;
var date = d.getFullYear() + "-" + pad(d.getMonth() + 1, 2, 0) + "-" + pad(d.getDate(), 2, 0);
emit({ProductId: sale.ProductID, Date:date, Method: sale.PurchaseMethod},
{Name: sale.PurchaseMethod, Count: 1})
};
var reduceDailySales = function(key, value) {
var res = {Name: 0, Count: 0};
value.forEach(function(value){
res.Name = value.Name;
res.Count += value.Count;
});
return res;
};
Current Output looks something like this:
{
"_id" : {
"ProductId" : 1,
"Date" : "2018-01-16",
"Method" : "Online"
},
"value" : {
"Name" : "Online",
"Count" : 3
}
}
Is there a way to achieve my desired output without map reducing again on the current output?
You can use aggregation pipeline to get the results instead of mapReduce, $group by ProductID and Date, with $project you can map counts to an array
added $out to write the results to new collection, removing it will return a cursor
db.prod.aggregate([
{$group : {
_id : {ProductID : "$ProductID", Date : "$Date"},
onlineCount : {$sum : {$cond : [{$eq : ["$PurchaseMethod", "Online"]}, "$Count" , 0]}},
storeCount : {$sum : {$cond : [{$eq : ["$PurchaseMethod", "In Store"]}, "$Count" , 0]}}
}
},
{$project : {
_id : 0,
ProductID : "$_id.ProductID",
Date : "$_id.Date",
counts : [{Name: "In Store", Count: "$storeCount"},{Name : "Online", Count: "$onlineCount"}]
}},
{$out : "count_stats"}
]).pretty()
collection
> db.prod.find()
{ "_id" : ObjectId("5a98ce4a62f54862fc7cd1f5"), "ProductID" : 1, "PurchaseMethod" : "In Store", "Date" : "2018-01-16", "Count" : 5 }
{ "_id" : ObjectId("5a98ce4a62f54862fc7cd1f6"), "ProductID" : 1, "PurchaseMethod" : "Online", "Date" : "2018-01-16", "Count" : 2 }
>
result
> db.count_stats.find()
{ "_id" : ObjectId("5a98d3366a5f43b12a39b4ac"), "ProductID" : 1, "Date" : "2018-01-16", "counts" : [ { "Name" : "In Store", "Count" : 5 }, { "Name" : "Online", "Count" : 2 } ] }
>
if you want to use mapReduce, you can use finalize to reduce or transform the result further
db.prod.mapReduce(
<map>,
<reduce>,
{
out: <collection>,
finalize: <function>
}
)

I don't know how to implementate the function reduce

I do not know how to implement the reduce function.
I have output from my map a document as ...
for example two documents
key "_id":"AD"
"values"
{
"numtweets" : 1,
"hastags" : {
"OPINIÓ" : 1,
"debat" : 1,
"inexistent" : 1,
"tampoc" : 1,
"promet" : 1,
"gaire" : 1,
"diàleg" : 1
}
}
and other
{
"numtweets" : 1,
"hastags" : {
"other" : 1,
"debat" : 1,
"inexistent" : 1,
"another" : 1,
}
}
I need a function that reduces all sum so that
"numtweets" : 2,
"dicwords" : {
"OPINIÓ" : 1,
"debat" : 2,
"inexistent" : 2,
"tampoc" : 1,
"promet" : 1,
"gaire" : 1,
"diàleg" : 1,
"other" : 1,
"another" : 1
},
"lisuser" : {
"user2" : 1
}
if my json values was only nuwtweets the reduce is
function(key, values) {
return Array.sum(values);
};
if mi json values only was hastags (without numtweets and lisuser) the reduce function was
var r = function(key, values) {
result = {}
values.forEach(function(val) {
for (hashtag in val) {
if (hashtag in result) {
result[hashtag] = result[hashtag] + val[hashtag]
} else {
result[hashtag] = val[hashtag]
}
}
});
return(result)
};
But I don't know how mix the 2 reduce function.
My level of mongodb and JavaScript is very low.
Thanks
Try this:
let data = values.reduce((finalObj, ele) => {
finalObj.numtweets += ele.numtweets
for (key in ele.hastags) {
if(ele.hastags.hasOwnProperty(key)) {
if(finalObj.hastags[key])
finalObj.hastags[key] += ele.hastags[key]
else
finalObj.hastags[key] = ele.hastags[key]
}
}
return finalObj
})
console.log(data)
Where values is array for objects which you want reduce to one object

Build key value array from nested JSON

I have the following JSON object. I need to convert it in javascript into a one-level associative array. (key-value pairs) I've written a recursive function but can't seem to get it to work the way I want it to. Kindly refer to the example below for what I've written:
{
"Title" : "Test Match",
"Players" : [
{
"ID" : 1,
"Name" : "Ma Couella",
"Results" : [2,2,0],
"TeamMates" : [
{
"ID" : 2,
"Age" : 24,
"LikesToWin" : false
}
]
}, {
"ID" : 22,
"Name" : "Shawn Kato",
"Results" : [2,1,0],
"TeamMates" : [
{
"Name" : "Gerald Anderson",
"Age" : 24,
"LikesToWin" : false
}
]
}
],
"Referees" : [
{
"ID" : 10,
"Name" : "Janice Tieu",
"YearsAsReferee" : 10,
"EmploymentHistory" : [
{
"JobTitle" : "Senior Referee",
"YearsOnTheJob" : 20,
"FavouriteMatchesRefereeed" : [
{
"Title" : "Duran vs Leonard 1",
"Year" : 1992,
"Description" : "I was refereeing the test match but I put on make up so Duran lost the bout."
}, {
"Title" : "Duran vs Leonard 2",
"Year" : 1994,
"Description" : "I was refereeing the second match but I put on make up so Duran lost another bout."
}
]
}, {
"JobTitle" : "Juniour Refereee",
"YearsOnTheJob" : 3,
"FavouriteMatchesRefereeed" : [
{
"Title" : "Mexican Figher Jones vs Anna Himley",
"Year" : 1972,
"Description" : "I coached this match. Hehe."
}, {
"Title" : "Jennifer from the block 2",
"Year" : 1982,
"Description" : "I coached this other match. Hehe."
}
]
}
]
}
]
}
Example of expected associative array below:
[
"Title" => "Test Match",
"Players[0][ID]" => 0,
// rest of the array...
"Players[0][Teammates][0][ID]" => 2,
// rest of the array...
"Referees[0][EmploymentHistory][FavouriteMatchesRefereeed][Title]" => "Duran vs Leonard 1"
]
Here is what I've done so far (javascript)
function converJSONToStrings(values, prevParent){
console.log(values);
var result = [];
for (var key in values)
{
var value = values[key];
if(value.constructor === Array) {
// console.log("Found object array in key", key );
for(var x = 0; x<value.length; x++){
result[key + '[' + x + ']'] = converJSONToStrings(value[x]);
}
} else if (typeof value == 'object') {
for(var x in value){
result[key + '[' + x + ']'] = converJSONToStrings(value[x]);
}
} else {
result[key] = value;
}
}
return result;
}
Finally came up with a solution! Code below...
/**
* Formats the JSON result to an associative array, concatenating child and parent keys
*/
var convertJSONAssoc = function(obj, firstlevel) {
// consider string, number and boolean values in JSON as the last
// elements and can't be recursed into any further
if (typeof obj == 'string' || typeof obj == 'number' || typeof obj == 'boolean') {
var ab = [];
var bc = {};
bc.key = '';
bc.val = obj;
ab.push(bc);
return ab;
}
// the top most call which builds the final result
if (firstlevel) {
var result = {};
for (key in obj) {
var val = obj[key];
var s = convertJSONAssoc(val, false);
for (var o = 0; o < s.length; o++) {
var v = s[o];
result[key + v['key']] = v['val'];
}
}
return result;
} else {
// this is where the recursion happens. As long as objects are found,
// we use the code below to keep on parsing obj keys
var paths = [];
for (var key in obj) {
var val = obj[key];
var s = convertJSONAssoc(val, false);
for (var o = 0; o < s.length; o++) {
var v = s[o];
var de = {};
de.key = "[" + key + "]" + v['key'];
de.val = v['val'];
paths.push(de);
}
}
return paths;
}
}
This is an old question that came back up because of edits from the OP. Modern techniques make this a bit easier than it was when the question was written.
This uses two functions. pathEntries I use to turn objects into a similar format that I find more useful, something like
[
[['Title'], 'Test Match'],
[['Players', 0, 'ID'], 1],
[['Players', 0, 'Name'], 'Ma Couella,
// ...
[['Referees', 0, 'EmploymentHistory', 1, 'FavouriteMatchesRefereeed', 1, 'Year'], 1982],
[['Referees', 0, 'EmploymentHistory', 1, 'FavouriteMatchesRefereeed', 1, 'Description'], 'I coached this other match. Hehe.']
]
Then convert, with a combination of key mapping and Object.fromEntries (which is easy to shim if you need to support environments without it), turns this into your required format. It looks like this:
const pathEntries = (obj) =>
Object (obj) === obj
? Object .entries (obj) .flatMap (
([k, x]) => pathEntries (x) .map (([p, v]) => [[k, ... p], v])
)
: [[[], obj]]
const convert = (obj) =>
Object .fromEntries (
pathEntries (obj) .map (([k, v]) => [
k[0] + k .slice (1) .map (n => `[${n}]`) .join (''),
v
])
)
const data = {Title: "Test Match", Players: [{ID: 1, Name: "Ma Couella", Results: [2, 2, 0], TeamMates: [{ID: 2, Age: 24, LikesToWin: !1}]}, {ID: 22, Name: "Shawn Kato", Results: [2, 1, 0], TeamMates: [{Name: "Gerald Anderson", Age: 24, LikesToWin: !1}]}], Referees: [{ID: 10, Name: "Janice Tieu", YearsAsReferee: 10, EmploymentHistory: [{JobTitle: "Senior Referee", YearsOnTheJob: 20, FavouriteMatchesRefereeed: [{Title: "Duran vs Leonard 1", Year: 1992, Description: "I was refereeing the test match but I put on make up so Duran lost the bout."}, {Title: "Duran vs Leonard 2", Year: 1994, Description: "I was refereeing the second match but I put on make up so Duran lost another bout."}]}, {JobTitle: "Juniour Refereee", YearsOnTheJob: 3, FavouriteMatchesRefereeed: [{Title: "Mexican Figher Jones vs Anna Himley", Year: 1972, Description: "I coached this match. Hehe."}, {Title: "Jennifer from the block 2", Year: 1982, Description: "I coached this other match. Hehe."}]}]}]}
console .log (convert (data))
.as-console-wrapper {max-height: 100% !important; top: 0}
Note that I often write pathEntries in terms of simpler getPaths and path functions. That is cleaner but less efficient as it requires extra traversals of the object.

How to group SAPUI5 OData before bind to control at client side?

I have an odata list for example in JSON notation:
var data = [
{"category" : "A", "value" : 1, "group" : "x"},
{"category" : "B", "value" : 2, "group" : "y"},
{"category" : "C", "value" : 3, "group" : "x"},
{"category" : "A", "value" : 4, "group" : "y"},
{"category" : "A", "value" : 5, "group" : "x"}
];
First of all I filter against group == x; Values left are:
var data = [
{"category" : "A", "value" : 1, "group" : "x"},
{"category" : "C", "value" : 3, "group" : "x"},
{"category" : "A", "value" : 5, "group" : "x"}
];
Now I would group (at client side) by category and sum up the values, so the result should be:
var data = [
{"category" : "A", "value" : 6, },
{"category" : "C", "value" : 3, },
];
After that I would bind the model to some SAPUI5 control.
But the grouping part seems not possible.
Does someone know a generic solution for that problem?
A potential use case:
var oDataset = new sap.viz.ui5.data.FlattenedDataset({
dimensions : [ {axis : 1, value : "{category}", name : "Category" } ],
measures : [ {value : "{value}", name : "Value" } ],
data : {
path : "/Data"
}
});
var oGraph = new sap.viz.ui5.Donut({
dataset : oDataset, // sap.viz.ui5.data.Dataset
});
The following code works for me. Query Northwind Order_Details service for a couple of Products via a generic binding, map reduce the the returned bindings and aggregate the Quantity of each order to the total Quanity sold, show the results in a column chart.
see jsBin Example
note sap.viz introduces Array.prototype.map and Array.prototype.reduce functions
var sURI = 'http://services.odata.org/Northwind/Northwind.svc/';
var oDataModel = new sap.ui.model.odata.ODataModel(sURI, true);
oDataModel.setSizeLimit(10000);
var oJSONModel = new sap.ui.model.json.JSONModel({}, 'jmodel');
// handle list of contexts
var handler = function(oEvent) {
var mapCallback = function(oContext) {
var obj = {};
obj.ProductID = oContext.getObject().ProductID,
obj.Quantity = oContext.getObject().Quantity
return obj;
};
var reduceCallback = function(aPrev, oCurr) {
var aNext = aPrev;
var bFound = false;
aNext.forEach(function(item) {
if (item.ProductID === oCurr.ProductID) {
bFound = true;
item.Quantity += oCurr.Quantity;
}
})
if (bFound === false) {
aNext.push(oCurr);
}
return aNext;
};
//release handler
oBindings.detachChange(handler);
var aTotals = oEvent.oSource.getContexts().map(mapCallback).reduce(reduceCallback, []);
oJSONModel.setData({
'Order_Totals': aTotals
});
};
// Filter all orders by 3 products
var oFilter1 = new sap.ui.model.Filter("ProductID", sap.ui.model.FilterOperator.EQ, '1');
var oFilter2 = new sap.ui.model.Filter("ProductID", sap.ui.model.FilterOperator.EQ, '68');
var oFilter3 = new sap.ui.model.Filter("ProductID", sap.ui.model.FilterOperator.EQ, '11');
var aFilter = [oFilter1, oFilter2, oFilter3];
// Sort by ProductID
var oSorter = new sap.ui.model.Sorter("ProductID", false, true);
// Reduce the returned payload by nominating need fields
var oSelect = {
select: 'ProductID,Quantity'
}
var oBindings = oDataModel.bindList("/Order_Details", null, oSorter, aFilter, oSelect);
// call OData service and handle results
oBindings.attachChange(handler);
oBindings.getContexts();
var oDataset = new sap.viz.ui5.data.FlattenedDataset({
dimensions: [{
axis: 1,
name: 'ProductID',
value: "{ProductID}"
}],
measures: [{
name: 'Quantity Sold',
value: '{Quantity}'
}],
data: {
path: "/Order_Totals"
}
});
var oColumnChart = new sap.viz.ui5.Column({
width: "80%",
height: "400px",
plotArea: {
'colorPalette': d3.scale.category20().range()
},
title: {
visible: true,
text: 'Qutantity Sold by Product'
},
dataset: oDataset
});
oColumnChart.setModel(oJSONModel);
For your final result i have written a code. Although it is not fully optimized but it works fine.
Here is your full code
var data = [
{"category" : "A", "value" : 1, "group" : "x"},
{"category" : "B", "value" : 2, "group" : "y"},
{"category" : "C", "value" : 3, "group" : "x"},
{"category" : "A", "value" : 4, "group" : "y"},
{"category" : "A", "value" : 5, "group" : "x"}
];
function groupBy( array , f )
{
var groups = {};
array.forEach( function( o )
{
if(o.group=="x")
{
var group = JSON.stringify( f(o) );
groups[group] = groups[group] || [];
groups[group].push( o );
}
});
return Object.keys(groups).map( function( group )
{
return groups[group];
})
}
var groupByGroup = groupBy(data, function(item)
{
return [item.group];
});
var groupByCategory = groupBy(groupByGroup[0], function(item)
{
return [item.category];
});
getData(groupByCategory);
function getData(groupByCategory)
{
var finalData=[];
for(var i=0;i< groupByCategory.length;i++)
{
var temp=0;
for(var j=0;j<groupByCategory[i].length;j++)
{
temp+=parseInt(groupByCategory[i][j].value);
}
finalData.push({"category":groupByCategory[i][0].category, "value":temp})
}
console.log(finalData);//final data is your required results
}
If you want to compare using two parameter .Suppose you want to compare by category as well as group then in groupByGroup function you have to write as follow
groupByCategory = groupBy(groupByGroup[0], function(item)
{
return [item.category,item.group];
})
and remove if(o.group=="x") from groupBy function.
This problem can be solved easily using underscore.js.For help see this question
Underscore.js: Sum of items in a collection
Hope this will help you.

How to sort array inside collection record in MongoDB?

I have a collection of students, each with a record that looks like the following and I want to sort the scores array in descending order of score.
what does that incantation look like on the mongo shell?
> db.students.find({'_id': 1}).pretty()
{
"_id" : 1,
"name" : "Aurelia Menendez",
"scores" : [
{
"type" : "exam",
"score" : 60.06045071030959
},
{
"type" : "quiz",
"score" : 52.79790691903873
},
{
"type" : "homework",
"score" : 71.76133439165544
},
{
"type" : "homework",
"score" : 34.85718117893772
}
]
}
I'm trying this incantation....
doc = db.students.find()
for (_id,score) in doc.scores:
print _id,score
but it's not working.
You will need to manipulate the embedded array in your application code or using the new Aggregation Framework in MongoDB 2.2.
Example aggregation in the mongo shell:
db.students.aggregate(
// Initial document match (uses index, if a suitable one is available)
{ $match: {
_id : 1
}},
// Expand the scores array into a stream of documents
{ $unwind: '$scores' },
// Filter to 'homework' scores
{ $match: {
'scores.type': 'homework'
}},
// Sort in descending order
{ $sort: {
'scores.score': -1
}}
)
Sample output:
{
"result" : [
{
"_id" : 1,
"name" : "Aurelia Menendez",
"scores" : {
"type" : "homework",
"score" : 71.76133439165544
}
},
{
"_id" : 1,
"name" : "Aurelia Menendez",
"scores" : {
"type" : "homework",
"score" : 34.85718117893772
}
}
],
"ok" : 1
}
Starting in Mongo 5.2, it's the exact use case for the new $sortArray aggregation operator:
// {
// name: "Aurelia Menendez",
// scores: [
// { type: "exam", score: 60.06 }
// { type: "quiz", score: 52.79 }
// { type: "homework", score: 71.76 }
// { type: "homework", score: 34.85 }
// ]
// }
db.collection.aggregate([
{ $set: {
scores: {
$sortArray: {
input: "$scores",
sortBy: { score: -1 }
}
}
}}
])
// {
// name: "Aurelia Menendez",
// scores: [
// { type: "homework", score: 71.76 },
// { type: "exam", score: 60.06 },
// { type: "quiz", score: 52.79 },
// { type: "homework", score: 34.85 }
// ]
// }
This:
sorts ($sortArray) the scores array (input: "$scores")
by applying a sort on scores (sortBy: { score: -1 })
without having to apply a combination of expensive $unwind, $sort and $group stages
Since this question can be managed in different ways i want to say that another solution is "insert and sort", in this way you will get the Ordered array at the moment you will made a Find().
Consider this data:
{
"_id" : 5,
"quizzes" : [
{ "wk": 1, "score" : 10 },
{ "wk": 2, "score" : 8 },
{ "wk": 3, "score" : 5 },
{ "wk": 4, "score" : 6 }
]
}
Here we will update the Document, make the Sort.
db.students.update(
{ _id: 5 },
{
$push: {
quizzes: {
$each: [ { wk: 5, score: 8 }, { wk: 6, score: 7 }, { wk: 7, score: 6 } ],
$sort: { score: -1 },
$slice: 3 // keep the first 3 values
}
}
}
)
Result is:
{
"_id" : 5,
"quizzes" : [
{ "wk" : 1, "score" : 10 },
{ "wk" : 2, "score" : 8 },
{ "wk" : 5, "score" : 8 }
]
}
Documentation:
https://docs.mongodb.com/manual/reference/operator/update/sort/#up._S_sort
That's how we could solve this with JS and mongo console:
db.students.find({"scores.type": "homework"}).forEach(
function(s){
var sortedScores = s.scores.sort(
function(a, b){
return a.score<b.score && a.type=="homework";
}
);
var lowestHomeworkScore = sortedScores[sortedScores.length-1].score;
db.students.update({_id: s._id},{$pull: {scores: {score: lowestHomeworkScore}}}, {multi: true});
})
In order to sort array, follow these steps:
1) use unwind to iterate through array
2) sort array
3) use group to merge objects of array into one array
4) then project other fields
Query
db.taskDetails.aggregate([
{$unwind:"$counter_offer"},
{$match:{_id:ObjectId('5bfbc0f9ac2a73278459efc1')}},
{$sort:{"counter_offer.Counter_offer_Amount":1}},
{$unwind:"$counter_offer"},
{"$group" : {_id:"$_id",
counter_offer:{ $push: "$counter_offer" },
"task_name": { "$first": "$task_name"},
"task_status": { "$first": "$task_status"},
"task_location": { "$first": "$task_location"},
}}
]).pretty()
Here is the java code which can be used to find out the lowest score in the array and remove it.
public class sortArrayInsideDocument{
public static void main(String[] args) throws UnknownHostException {
MongoClient client = new MongoClient();
DB db = client.getDB("school");
DBCollection lines = db.getCollection("students");
DBCursor cursor = lines.find();
try {
while (cursor.hasNext()) {
DBObject cur = cursor.next();
BasicDBList dbObjectList = (BasicDBList) cur.get("scores");
Double lowestScore = new Double(0);
BasicDBObject dbObject = null;
for (Object doc : dbObjectList) {
BasicDBObject basicDBObject = (BasicDBObject) doc;
if (basicDBObject.get("type").equals("homework")) {
Double latestScore = (Double) basicDBObject
.get("score");
if (lowestScore.compareTo(Double.valueOf(0)) == 0) {
lowestScore = latestScore;
dbObject = basicDBObject;
} else if (lowestScore.compareTo(latestScore) > 0) {
lowestScore = latestScore;
dbObject = basicDBObject;
}
}
}
// remove the lowest score here.
System.out.println("object to be removed : " + dbObject + ":"
+ dbObjectList.remove(dbObject));
// update the collection
lines.update(new BasicDBObject("_id", cur.get("_id")), cur,
true, false);
}
} finally {
cursor.close();
}
}
}
It's easy enough to guess, but anyway, try not cheat with mongo university courses because you won't understand basics then.
db.students.find({}).forEach(function(student){
var minHomeworkScore,
scoresObjects = student.scores,
homeworkArray = scoresObjects.map(
function(obj){
return obj.score;
}
);
minHomeworkScore = Math.min.apply(Math, homeworkArray);
scoresObjects.forEach(function(scoreObject){
if(scoreObject.score === minHomeworkScore){
scoresObjects.splice(scoresObjects.indexOf(minHomeworkScore), 1);
}
});
printjson(scoresObjects);
});
Order Title and Array title also and return whole collection data Collection name is menu
[
{
"_id": "5f27c5132160a22f005fd50d",
"title": "Gift By Category",
"children": [
{
"title": "Ethnic Gift Items",
"s": "/gift?by=Category&name=Ethnic"
},
{
"title": "Novelty Gift Items",
"link": "/gift?by=Category&name=Novelty"
}
],
"active": true
},
{
"_id": "5f2752fc2160a22f005fd50b",
"title": "Gift By Occasion",
"children": [
{
"title": "Gifts for Diwali",
"link": "/gift-for-diwali"
},
{
"title": "Gifts for Ganesh Chathurthi",
"link": "/gift-for-ganesh-chaturthi",
}
],
"active": true
}
]
Query as below
let menuList = await Menu.aggregate([
{
$unwind: '$children'
},
{
$sort:{"children.title":1}
},
{
$group : { _id : "$_id",
root: { $mergeObjects: '$$ROOT' },
children: { $push: "$children" }
}
},
{
$replaceRoot: {
newRoot: {
$mergeObjects: ['$root', '$$ROOT']
}
}
},
{
$project: {
root: 0
}
},
{
$match: {
$and:[{'active':true}],
}
},
{
$sort:{"title":1}
}
]);
I believe you are doing M101P: MongoDB for Developers where homework 3.1 is to remove the lower one from two homework scores. Since aggregations were not taught up to that point you can do something like this:
import pymongo
conn = pymongo.MongoClient('mongodb://localhost:27017')
db = conn.school
students = db.students
for student_data in students.find():
smaller_homework_score_seq = None
smaller_homework_score_val = None
for score_seq, score_data in enumerate(student_data['scores']):
if score_data['type'] == 'homework':
if smaller_homework_score_seq is None or smaller_homework_score_val > score_data['score']:
smaller_homework_score_seq = score_seq
smaller_homework_score_val = score_data['score']
students.update({'_id': student_data['_id']}, {'$pop': {'scores': smaller_homework_score_seq}})
This is my approach using pyMongo, the Python driver to MongoDB:
import pymongo
conn = pymongo.MongoClient('mongodb://localhost')
def remove_lowest_hw():
db = conn.school
students = db.students
# first sort scores in ascending order
students.update_many({}, {'$push':{'scores':{'$each':[], '$sort':{'score': 1}}}})
# then collect the lowest homework score for each student via projection
cursor = students.find({}, {'scores':{'$elemMatch':{'type':'homework'}}})
# iterate over each student, trimming each of the lowest homework score
for stu in cursor:
students.update({'_id':stu['_id']}, {'$pull':{'scores':{'score':stu['scores'][0]['score']}}})
remove_lowest_hw()
conn.close()
this work for me, it is a little rough code but the results of the lowest tasks for each student are correct.
var scores_homework = []
db.students.find({"scores.type": "homework"}).forEach(
function(s){
s.scores.forEach(
function(ss){
if(ss.type=="homework"){
ss.student_id = s._id
scores_homework.push(ss)
}
}
)
})
for(i = 0; i < scores_homework.length; i++)
{
var b = i+1;
var ss1 = scores_homework[i];
var ss2 = scores_homework[b];
var lowest_score = {};
if(ss1.score > ss2.score){
lowest_score.type = ss2.type;
lowest_score.score = ss2.score;
db.students.update({_id: ss2.student_id},{$pull: {scores: {score: lowest_score.score}}});
}else if(ss1.score < ss2.score){
lowest_score.type = ss1.type;
lowest_score.score = ss1.score;
db.students.update({_id: ss1.student_id},{$pull: {scores: {score: lowest_score.score}}});
}else{
lowest_score.type = ss1.type;
lowest_score.score = ss1.score;
db.students.update({_id: ss1.student_id},{$pull: {scores: {score: lowest_score.score}}});
}
i++
}
This is how I have implemented in Java (Have kept it simple so that it's easier to understand) -
Approach :
Get scores array from student collection
Get all score values from scores array where type == homework
Sort the score values so that lowest becomes 1st element [score.get(0)]
Then, loop through the main scores and create new copy of scores array while skipping elements where type == homework && score == scores.get(0)
Finally, update the new scores array to student document.
Below is working Java code:
public void removeLowestScore(){
//Create mongo client and database connection and get collection
MongoClient client = new MongoClient("localhost");
MongoDatabase database = client.getDatabase("school");
MongoCollection<Document> collection = database.getCollection("students");
FindIterable<Document> docs = collection.find();
for (Document document : docs) {
//Get scores array
ArrayList<Document> scores = document.get("scores", ArrayList.class);
//Create a list of scores where type = homework
List<Double> homeworkScores = new ArrayList<Double>();
for (Document score : scores) {
if(score.getString("type").equalsIgnoreCase("homework")){
homeworkScores.add(score.getDouble("score"));
}
}
//sort homework scores
Collections.sort(homeworkScores);
//Create a new list to update into student collection
List<Document> newScoresArray = new ArrayList<Document>();
Document scoreDoc = null;
//Below loop populates new score array with eliminating lowest score of "type" = "homework"
for (Document score : scores) {
if(score.getString("type").equalsIgnoreCase("homework") && homeworkScores.get(0) == score.getDouble("score")){
continue;
}else{
scoreDoc = new Document("type",score.getString("type"));
scoreDoc.append("score",score.getDouble("score"));
newScoresArray.add(scoreDoc);
}
}
//Update the scores array for every student using student _id
collection.updateOne(Filters.eq("_id", document.getInteger("_id")), new Document("$set",new Document("scores",newScoresArray)));
}
}
Certainly it's late, but I just want to contribute my own solution on Mongo Shell:
var students = db.getCollection('students').find({});
for(i = 0 ; i < students.length(); i++) {
var scores = students[i].scores;
var tmp = [];
var min = -1 ;
var valueTmp = {};
for(j = 0 ; j < scores.length; j++) {
if(scores[j].type != 'homework') {
tmp.push(scores[j]);
} else {
if (min == -1) {
min = scores[j].score;
valueTmp = scores[j];
} else {
if (min > scores[j].score) {
min = scores[j].score;
tmp.push(valueTmp);
valueTmp = scores[j];
} else {
tmp.push(scores[j]);
}
}
}
}
db.students.updateOne({_id:students[i]._id},
{$set:{scores:tmp}});
}
the answer of #Stennie is fine, maybe a $group operator would be useful to keep the original document, without exploding it in many documents (one by score).
I just add another solution when using javascript for your application.
if you query only one document, it's sometimes easier to sort the embedded array by JS, instead of doing an aggregate.
When your document has a lot of fields, it's even better than using $push operator, otherwise you've to push all the fields one by one, or use $$ROOT operator (am I wrong ?)
My example code uses Mongoose.js :
Suppose you have initialized you Students model.
// Sorting
function compare(a, b) {
return a.score - b.score;
}
Students.findById('1', function(err, foundDocument){
foundDocument.scores = foundDocument.scores.sort(compare);
// do what you want here...
// foundModel keeps all its fields
});
sort by the score can be simple like:
db.students.find({_id:137}).sort({score:-1}).pretty()
but you need to find the one for type:homework ...
it should be something like this:
db.students.find().sort(scores: ({"score":-1}));

Categories