Mongodb $subtract document value from the next one - mongodb

I have such a collection of documents:
{ _id: 1, Meters: { gasmeter: 1000.0 } }
{ _id: 2, Meters: { gasmeter: 1007.0 } }
{ _id: 3, Meters: { gasmeter: 1010.0 } }
And I am trying to get the difference between the gasmeter elements as such:
{ difference: 7 } // Difference between _id=1 and _id=2
{ difference: 3 } // Difference between _id=2 and _id=3
I've tried:
db.MeterData.aggregate([{$project: { item: 1, difference: {$subtract: [ {$add: [ "$Meters.gasmeter","$Meters.gasmeter" ] }, "$Meters.gasmeter" ] }}}])
But this does not work. I still get the same values.
Any idea how to do that aggregation with mongodb?

Since I'm not entirely sure how you wish to solve the problem (in C# or mongo itself). I've come up with this in Mongo.
db.MeterData.find().forEach(
function (doc) {
var next = db.Sum.findOne({
_id: {
"$gt": NumberInt(doc._id)
}
})
if (next != null) {
var diff = next.Meters.gasmeter - doc.Meters.gasmeter;
printjson("{ difference : " + diff + "} - Difference between Id=" + doc._id + " and Id=" + next._id);
}
})
Which returns
"{ difference : 7} - Difference between Id=1 and Id=2"
"{ difference : 3} - Difference between Id=2 and Id=3"
I hope this helps you a bit.

Starting in Mongo 5, it's a perfect use case for the new $setWindowFields aggregation operator:
// { id: 1, gasmeter: 1000 }
// { id: 2, gasmeter: 1007 }
// { id: 3, gasmeter: 1010 }
db.collection.aggregate([
{ $setWindowFields: {
sortBy: { id: 1 },
output: {
difference: {
$push: "$gasmeter",
window: { range: [-1, "current"] }
}
}
}},
// { id: 1, gasmeter: 1000, difference: [1000] }
// { id: 2, gasmeter: 1007, difference: [1000, 1007] }
// { id: 3, gasmeter: 1010, difference: [1007, 1010] }
{ $match: { $expr: { $eq: [{ $size: "$difference" }, 2] } } },
// { id: 2, gasmeter: 1007, difference: [1000, 1007] }
// { id: 3, gasmeter: 1010, difference: [1007, 1010] }
{ $set: {
difference: { $subtract: [{ $last: "$difference" }, { $first: "$difference" }] }
}}
])
// { id: 2, gasmeter: 1007, difference: 7 }
// { id: 3, gasmeter: 1010, difference: 3 }
This:
starts with a $setWindowFields aggregation stage which adds the difference field in each document (output: { difference: { ... }})
as an array $push of gasmeters ($sum: "$gasmeter")
from the specified span of documents (the window) which here is the "current" document and the previous one "-1": window: { range: [-1, "current"] }
then filters out the first document that doesn't have a previous document (i.e. we can't diff its value with a previous document's value):
to do so we simply check that the size of the array produced with the previous stage is 2: { $match: { $expr: { $eq: [{ $size: "$difference" }, 2] } } },
and finally substracts the 2 values in the array to get the actual difference: $subtract: [{ $last: "$difference" }, { $first: "$difference" }]

Related

MongoDB get count of field per season from MM/DD/YYYY date field

I am facing a problem in MongoDB. Suppose, I have the following collection.
{ id: 1, issueDate: "07/05/2021", code: "31" },
{ id: 2, issueDate: "12/11/2020", code: "14" },
{ id: 3, issueDate: "02/11/2021", code: "98" },
{ id: 4, issueDate: "01/02/2021", code: "14" },
{ id: 5, issueDate: "06/23/2020", code: "14" },
{ id: 6, issueDate: "07/01/2020", code: "31" },
{ id: 7, issueDate: "07/05/2022", code: "14" },
{ id: 8, issueDate: "07/02/2022", code: "20" },
{ id: 9, issueDate: "07/02/2022", code: "14" }
The date field is in the format MM/DD/YYYY. My goal is to get the count of items with each season (spring (March-May), summer (June-August), autumn (September-November) and winter (December-February).
The result I'm expecting is:
count of fields for each season:
{ "_id" : "Summer", "count" : 6 }
{ "_id" : "Winter", "count" : 3 }
top 2 codes (first and second most recurring) per season:
{ "_id" : "Summer", "codes" : {14, 31} }
{ "_id" : "Winter", "codes" : {14, 98} }
How can this be done?
You should never store date/time values as string, store always proper Date objects.
You can use $setWindowFields opedrator for that:
db.collection.aggregate([
// Convert string into Date
{ $set: { issueDate: { $dateFromString: { dateString: "$issueDate", format: "%m/%d/%Y" } } } },
// Determine the season (0..3)
{
$set: {
season: { $mod: [{ $toInt: { $divide: [{ $add: [{ $subtract: [{ $month: "$issueDate" }, 1] }, 1] }, 3] } }, 4] }
}
},
// Count codes per season
{
$group: {
_id: { season: "$season", code: "$code" },
count: { $count: {} },
}
},
// Rank occurrence of codes per season
{
$setWindowFields: {
partitionBy: "$_id.season",
sortBy: { count: -1 },
output: {
rank: { $denseRank: {} },
count: { $sum: "$count" }
}
}
},
// Get only top 2 ranks
{ $match: { rank: { $lte: 2 } } },
// Final grouping
{
$group: {
_id: "$_id.season",
count: { $first: "$count" },
codes: { $push: "$_id.code" }
}
},
// Some cosmetic for output
{
$set: {
season: {
$switch: {
branches: [
{ case: { $eq: ["$_id", 0] }, then: 'Winter' },
{ case: { $eq: ["$_id", 1] }, then: 'Spring' },
{ case: { $eq: ["$_id", 2] }, then: 'Summer' },
{ case: { $eq: ["$_id", 3] }, then: 'Autumn' },
]
}
}
}
}
])
Mongo Playground
I will give you clues,
You need to use $group with _id as $month on issueDate, use accumulator $sum to get month wise count.
You can divide month by 3, to get modulo, using $toInt, $divide, then put them into category using $cond.
Another option:
db.collection.aggregate([
{
$addFields: {
"season": {
$switch: {
branches: [
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"06",
"07",
"08"
]
]
},
then: "Summer"
},
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"03",
"04",
"05"
]
]
},
then: "Spring"
},
{
case: {
$in: [
{
$substr: [
"$issueDate",
0,
2
]
},
[
"12",
"01",
"02"
]
]
},
then: "Winter"
}
],
default: "No date found."
}
}
}
},
{
$group: {
_id: {
s: "$season",
c: "$code"
},
cnt1: {
$sum: 1
}
}
},
{
$sort: {
cnt1: -1
}
},
{
$group: {
_id: "$_id.s",
codes: {
$push: "$_id.c"
},
cnt: {
$sum: "$cnt1"
}
}
},
{
$project: {
_id: 0,
season: "$_id",
count: "$cnt",
codes: {
"$slice": [
"$codes",
2
]
}
}
}
])
Explained:
Add one more field for season based on $switch per month(extracted from issueDate string)
Group to collect per season/code.
$sort per code DESCENDING
group per season to form an array with most recurring codes in descending order.
Project the fields to the desired output and $slice the codes to limit only to the fist two most recurring.
Comment:
Indeed keeping dates in string is not a good idea in general ...
Playground

MongoDB Query Dynamic keys

Need to find quoteIds where having different values and keys should start with 1 or 2 or 3. Could you please help.
{
"quoteId": 1,
"screening": {
"101": 1,
"201": 1,
"301": 1,
"100": 1,
"200": 1,
"300": 1,
"111": 1,
"211": 1,
"311": 1
}
}
{
"quoteId": 2,
"screening": {
"101": 1,
"201": 1,
"301": 1,
"100": 1,
"200": 1,
"300": 1,
"111": 1,
"211": 2,
"311": 1
}
}
$set - Create screenings array field, by converting object (key-value pair) to multiple documents (via $objectToArray) and fulfill the regex with starting of 1 or 2 or 3 in $filter.
$match - Filter documents that screenings is not an empty array.
$unset - Remove screenings field.
db.collection.aggregate([
{
$set: {
screenings: {
$filter: {
input: {
"$objectToArray": "$screening"
},
cond: {
"$regexMatch": {
input: "$$this.k",
regex: "^(1|2|3)"
}
}
}
}
}
},
{
$match: {
screenings: {
$ne: []
}
}
},
{
$unset: "screenings"
}
])
Sample Mongo Playground

MongoDB sum of all fields with integer values

inside the aggregation framework, it's possibile in some way, for each document like this below:
{
"Title": "Number orders",
"2021-03-16": 3,
"2021-03-15": 6,
"2021-03-19": 1,
"2021-03-14": 19
}
Obtain a new document like this?
{
"Title": "Number orders",
"2021-03-16": 3,
"2021-03-15": 6,
"2021-03-19": 1,
"2021-03-14": 19
"Total": 29
}
Basically, I want a new field that have inside the sum of all the values of the fields that are integer.
Another thing to take in consideration is that the date fields are dynamic, so one week could be like the one in the example, the following week the fields would become like
{
"Title": "Number orders",
"2021-03-23": 3,
"2021-03-22": 6,
"2021-03-26": 1,
"2021-03-21": 19
}
Thanks!
Demo - https://mongoplayground.net/p/724nerJUQtK
$$ROOT is the entire document, add total using $addFields use $sum to add them up and remove allData using $unset
db.collection.aggregate([
{ $addFields: { allData: { "$objectToArray": "$$ROOT" } } } },
{ $addFields: { "total": { $sum: "$allData.v" } } },
{ $unset: "allData" }
])
Based on your older question, I think this might help:
db.collection.aggregate([
{
$group: {
_id: {
dDate: "$deliveryDay",
name: "$plate.name"
},
v: { $sum: "$plate.quantity" }
}
},
{
$group: {
_id: "$_id.name",
Total: { $sum: "$v" },
array: {
$push: { k: "$_id.dDate", v: "$v" }
}
}
},
{
$addFields: {
array: {
$concatArrays: [
[{ k: "Title", v: "Number orders" }],
"$array",
[{ k: "Total", v: "$Total" }]
]
}
}
},
{
$replaceRoot: {
newRoot: { $arrayToObject: "$array" }
}
}
])
Output:
/* 1 */
{
"Title" : "Number orders",
"2021-01-16" : 2,
"Total" : 2
},
/* 2 */
{
"Title" : "Number orders",
"2021-01-14" : 1,
"2021-01-16" : 3,
"Total" : 4
}

mongodb average arrays across many documents

Using mongodb, I have a collection of documents where each document has a fixed length vector of floating point values such as below:
items = [
{"id": "1", "vec": [1, 2, 0]},
{"id": "2", "vec": [6, 4, 1]},
{"id": "3", "vec": [3, 2, 2]},
]
I would like to take the row wise average of these vectors. In this example I would expect the result to return
[ (1 + 6 + 3) / 3, (2 + 4 + 2) / 3, (0 + 1 + 2) / 3 ]
This answer is very close to what I am looking for, but as far as I can tell it will only work on vectors of size 2. mongoDB - average on array values
An answer has been provided that is not very performant for large arrays. For context I am using ~700 dimension vectors.
This should work: https://mongoplayground.net/p/PKXqmmW31nW
[
{
$group: {
_id: null,
a: {
$push: {
$arrayElemAt: ["$vec", 0]
}
},
b: {
$push: {
$arrayElemAt: ["$vec", 1]
}
},
c: {
$push: {
$arrayElemAt: ["$vec", 2]
}
}
}
},
{
$project: {
a: {
$avg: "$a"
},
b: {
$avg: "$b"
},
c: {
$avg: "$c"
}
}
}
]
Which outputs:
[
{
"_id": null,
"a": 3.3333333333333335,
"b": 2.6666666666666665,
"c": 1
}
]
Here's a more efficient without $avg operator. I'll leave other answer up for reference.
https://mongoplayground.net/p/rVERc8YjKZv
db.collection.aggregate([
{
$group: {
_id: null,
a: {
$sum: {
$arrayElemAt: ["$vec", 0]
}
},
b: {
$sum: {
$arrayElemAt: ["$vec", 1]
}
},
c: {
$sum: {
$arrayElemAt: ["$vec", 2]
}
},
totalDocuments: {
$sum: 1
}
}
},
{
$project: {
a: {
$divide: ["$a", "$totalDocuments"]
},
b: {
$divide: ["$b", "$totalDocuments"]
},
c: {
$divide: ["$c", "$totalDocuments"]
}
}
}
])
You can use $unwind to get values into separate documents, the key is to keep the index of the values. Then you can use $group by the index and calculate the average using the $avg operator.
db.collection.aggregate([
{
$unwind: {
path: "$vec",
includeArrayIndex: "i" // unwind and keep index
}
},
{
$group: {
_id: "$i", // group by index
avg: { $avg: "$vec" }
}
}, // at this stage, you already get all the values you need, in separate documents. The following stages will put all the values in an array
{
$sort: { _id: 1 }
},
{
$group: {
_id: null,
avg: { $push: "$avg" }
}
}
])
Mongo Playground

Query Mongo to get only documents different that previous document [duplicate]

Im wondering if the following is possible in MongoDB.
I have collection of documents that represent changes in some value in time:
{
"day" : ISODate("2018-12-31T23:00:00.000Z"),
"value": [some integer value]
}
There are no 'holes' in the data, I have entries for all days within some period.
Is it possible to query this collection to get only documents that has different value than previous one (when sorting by day asc)? For example, having following documents:
{ day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
{ day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
{ day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 }
I want to retrieve documents for 2018-04-01, 2018-04-03 and 2018-04-06 and only those since others don't have a change of value.
You need to get pairs of consecutive docs to detect the gap. For that you can push all documents into single array, and zip it with itself shifted 1 element from the head:
db.collection.aggregate([
{ $sort: { day: 1 } },
{ $group: { _id: null, docs: { $push: "$$ROOT" } } },
{ $project: {
pair: { $zip: {
inputs:[ { $concatArrays: [ [false], "$docs" ] }, "$docs" ]
} }
} },
{ $unwind: "$pair" },
{ $project: {
prev: { $arrayElemAt: [ "$pair", 0 ] },
next: { $arrayElemAt: [ "$pair", 1 ] }
} },
{ $match: {
$expr: { $ne: ["$prev.value", "$next.value"] }
} },
{ $replaceRoot:{ newRoot: "$next" } }
])
The rest is trivial - you unwind the array back to documents, compare the pairs, filter out the equal ones, and replaceRoot from what's left.
Starting in Mongo 5, it's a perfect use case for the new $setWindowFields aggregation operator:
// { day: ISODate("2019-04-01T00:00:00.000Z"), value: 10 } <=
// { day: ISODate("2019-04-02T00:00:00.000Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00.000Z"), value: 15 } <=
// { day: ISODate("2019-04-04T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-05T00:00:00.000Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00.000Z"), value: 10 } <=
db.collection.aggregate([
{ $setWindowFields: {
sortBy: { day: 1 },
output: { pair: { $push: "$value", window: { documents: [-1, "current"] } } }
}},
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10, pair: [ 10 ] }
// { day: ISODate("2019-04-02T00:00:00Z"), value: 10, pair: [ 10, 10 ] }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15, pair: [ 10, 15 ] }
// { day: ISODate("2019-04-04T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-05T00:00:00Z"), value: 15, pair: [ 15, 15 ] }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10, pair: [ 15, 10 ] }
{ $match: { $expr: { $or: [
{ $eq: [ { $size: "$pair" }, 1 ] }, // first doc doesn't have a previous doc
{ $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }
]}}},
{ $unset: ["pair"] }
])
// { day: ISODate("2019-04-01T00:00:00Z"), value: 10 }
// { day: ISODate("2019-04-03T00:00:00Z"), value: 15 }
// { day: ISODate("2019-04-06T00:00:00Z"), value: 10 }
This:
starts with a $setWindowFields aggregation stage which adds a pair field representing the current document's value and the previous document's value (output: { pair: { ... }}):
$setWindowFields provides for a given document a view of other documents (a window)
which in our case is the "current" document and the previous one "-1": window: { documents: [-1, "current"] }.
such that we build within this window an array of values: $push: "$value"
and note that we've made sure to sort documents by day: sortBy: { day: 1 }.
and then:
filters in the first document (which is remarquable by its array having only one element): { $eq: [ { $size: "$pair" }, 1 ] }
and filters out the following documents if their pair has the same values: { $ne: [ { $first: "$pair" }, { $last: "$pair" } ] }