Concat int array field inside an array of object into one string field in mongodb aggregate - mongodb

I would like to concat int array field values inside an array of objects into one string field after dividing them (by 10).
Heres the existing document format:
{
"no" : "2020921008981",
"date" : ISODate("2020-04-01T05:19:02.263+0000"),
"sale" : {
"soldItems" : [
{
"itemRefId" : "5b55ac7f0550de00210a3b24",
"soldPrice" : NumberInt(800),
},
{
"itemRefId" : "5b55ac7f0550de00210a3b25",
"soldPrice" : NumberInt(1000),
}
]
}
}
Expected result :
{
"no" : "2020921008981",
"date" : ISODate("2020-04-01T05:19:02.263+0000"),
"priceList" : "8.0 \n 10.0"
}
The attempt with $reduce :
priceList: {
$reduce: {
input: "$sale.soldItems.soldPrice",
initialValue: "",
in: {
$cond: [ { "$eq": [ { $toString: { $divide: [ "$$value", 10 ] } }, "" ] }, "$$this", { $concat: [ { $toString: { $divide: [ "$$value", 10 ] } }, "\n", "$$this" ] } ]
}
}
}
But end up getting "errmsg" : "$divide only supports numeric types, not string and double" error. Any idea would be appreciated.

db.case.aggregate([
{
$set: {
priceList: {
$reduce: {
input: {
$map: {
input: "$sale.soldItems.soldPrice",
in: { $toString: { $divide: ["$$this", 10] } }
}
},
initialValue: "",
in: { $concat: ["$$value", "$$this", " \n "] }
}
}
}
},
{
$project: {
_id: 0,
no: 1,
date: 1,
priceList: 1
}
}
])

Try the following aggregation query, where the idea is to:
First divide the field soldPrice by 10 or required divisor using $divide
Convert it into string and concat using $toString and $concat
the appender \n gets appended after each reduce op,remove that from the end using $rtrim
create the new field using $addFields
Query:
db.collection.aggregate([
{
$addFields: {
"itemPriceList": {
$rtrim: {
input: {
$reduce: {
input: "$salesOrder.purchaseItems",
initialValue: "",
in: {
$concat: [
"$$value",
{
$toString: {
$divide: [
"$$this.soldPrice",
10
]
}
},
"\n"
]
}
}
},
chars: "\n"
}
}
}
}
]);
Result:
[
{
"_id": ObjectId("5a934e000102030405000000"),
"caseNumber": "2020921008981",
"itemPriceList": "80\n100",
"salesOrder": {
"purchaseItems": [
{
"itemRefId": "5b55ac7f0550de00210a3b24",
"soldPrice": 800
},
{
"itemRefId": "5b55ac7f0550de00210a3b25",
"soldPrice": 1000
}
]
},
"startTime": ISODate("2016-05-18T16:00:00Z")
}
]
Plaground Test Link

Related

Count Both Outer and Inner embedded array in a single query

{
_id: ObjectId("5dbdacc28cffef0b94580dbd"),
"comments" : [
{
"_id" : ObjectId("5dbdacc78cffef0b94580dbf"),
"replies" : [
{
"_id" : ObjectId("5dbdacd78cffef0b94580dc0")
},
]
},
]
}
How to count the number of element in comments and sum with number of relies
My approach is do 2 query like this:
1. total elements of replies
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{ $unwind: "$comments",},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
2. count total elements of comments
db.posts.aggregate([
{$match: {_id:ObjectId("5dbdacc28cffef0b94580dbd")}},
{$project:{total:{$size:"$comments.replies"} , _id: 0} }
])
Then sum up both, do we have any better solution to write the query like return the sum of of total element comments + replies
You can use $reduce and $concatArrays to "merge" an inner "array of arrays" into a single list and measure the $size of that. Then simply $add the two results together:
db.posts.aggregate([
{ "$match": { _id:ObjectId("5dbdacc28cffef0b94580dbd") } },
{ "$addFields": {
"totalBoth": {
"$add": [
{ "$size": "$comments" },
{ "$size": {
"$reduce": {
"input": "$comments.replies",
"initialValue": [],
"in": {
"$concatArrays": [ "$$value", "$$this" ]
}
}
}}
]
}
}}
])
Noting that an "array of arrays" is the effect of an expression like $comments.replies, so hence the operation to make these into a single array where you can measure all elements.
Try using the $unwind to flatten the list you get from the $project before using $count.
This is another way of getting the result.
Input documents:
{ "_id" : 1, "array1" : [ { "array2" : [ { id: "This is a test!"}, { id: "test1" } ] }, { "array2" : [ { id: "This is 2222!"}, { id: "test 222" }, { id: "222222" } ] } ] }
{ "_id" : 2, "array1" : [ { "array2" : [ { id: "aaaa" }, { id: "bbbb" } ] } ] }
The query:
db.arrsizes2.aggregate( [
{ $facet: {
array1Sizes: [
{ $project: { array1Size: { $size: "$array1" } } }
],
array2Sizes: [
{ $unwind: "$array1" },
{ $project: { array2Size: { $size: "$array1.array2" } } },
],
} },
{ $project: { result: { $concatArrays: [ "$array1Sizes", "$array2Sizes" ] } } },
{ $unwind: "$result" },
{ $group: { _id: "$result._id", total1: { $sum: "$result.array1Size" }, total2: { $sum: "$result.array2Size" } } },
{ $addFields: { total: { $add: [ "$total1", "$total2" ] } } },
] )
The output:
{ "_id" : 2, "total1" : 1, "total2" : 2, "total" : 3 }
{ "_id" : 1, "total1" : 2, "total2" : 5, "total" : 7 }

Document transformation based type

Lets say I have a mongodb document
{
"id" : 1,
"types" : ["online" ,"offline"],
"applications" : [ ["online", "online"], ["offline"] ]
}
I would like to transformed into
{
"id" : 1,
"types" : [
{
"type" : "online",
"count" : 2,
"applications" : [ "online", "online"]
},
{
"type" : "offline",
"count" : 1,
"applications" : [ "offline" ]
}
]
}
here types and applications arrays can be of any size.
You can do that in two steps, use $map with $filter to match type with applications and then run $reduce to get count:
db.collection.aggregate([
{
$project: {
id: 1,
types: {
$map: {
input: "$types",
as: "type",
in: {
type: "$$type",
applications: {
$filter: {
input: "$applications",
as: "application",
cond: {
$allElementsTrue: {
$map: { input: "$$application", in: { $eq: [ "$$this", "$$type" ] } }
}
}
}
}
}
}
}
}
},
{
$addFields: {
types: {
$map: {
input: "$types",
in: {
$mergeObjects: [
"$$this",
{
count: {
$reduce: {
input: "$$this.applications",
initialValue: 0,
in: { $add: [ "$$value", { $size: "$$this" } ] }
}
}
}
]
}
}
}
}
}
])
You can decide whether it's better to use $allElementsTrue or $anyElementTrue when building your filtering criteria.
Mongo Playground

How can I compare two string format times?

I have a collection called "project" which is having a field expected time and actual time both are in string format
{
"_id" : ObjectId("5ce7455d77af2d1143f84d49"),
"project_name" : "p1",
"expected" : "0:11:30",
"actual" : "7:30:00",
}
How can I compare two string format times using mongodb?
I want to find if actual time is more than expected
You can use $split with $toInt (MongoDB 4.0 or newer) to convert your string values to a number of seconds and then use $expr to compare both fields:
db.col.aggregate([
{
$addFields: {
expected: {
$let: {
vars: {
parts: {
$split: [ "$expected", ":" ]
}
},
in: {
$sum: [
{ $toInt: { $arrayElemAt: [ "$$parts", 2 ] } },
{ $multiply: [ 60, { $toInt: { $arrayElemAt: [ "$$parts", 1 ] } } ] },
{ $multiply: [ 3600, { $toInt: { $arrayElemAt: [ "$$parts", 0 ] } } ] }
]
}
}
},
actual: {
$let: {
vars: {
parts: {
$split: [ "$actual", ":" ]
}
},
in: {
$sum: [
{ $toInt: { $arrayElemAt: [ "$$parts", 2 ] } },
{ $multiply: [ 60, { $toInt: { $arrayElemAt: [ "$$parts", 1 ] } } ] },
{ $multiply: [ 3600, { $toInt: { $arrayElemAt: [ "$$parts", 0 ] } } ] }
]
}
}
}
}
},
{
$match: {
$expr: { $gt: [ "$expected", "$actual" ] }
}
}
])
You can convert time to any date you want using $dateFromString operator and then can easily use $lte $gte to perform simple match operations.
db.collection.find({
"$expr": {
"$gt": [
{ "$dateFromString": {
"dateString": {
"$concat": ["2018-09-19", "T", "$actual"]
}
}},
{ "$dateFromString": {
"dateString": {
"$concat": ["2018-09-19", "T", "$expected"]
}
}}
]
}
})

MongoDB: aggregating by property names

I have a collection within which each document looks a bit like this:
{
"_id" : ObjectId("5b9fe6010c969210d442a377"),
"statuses" : {
"SEARCHING" : 3
},
"regions" : {
"eu-central-1" : 1,
"us-west-2": 2
}
}
I want to group and transform this into a result set like this:
eu-central-1|us-west-2
1|2
Is this possible in one step/query?
This is probably what you want:
db.collection.aggregate({
$project: {
"regions": { $objectToArray: "$regions" } // convert sub-document into an array of key-value-pairs in order to get hold of the field names
}
}, {
$unwind: "$regions" // flatten the "regions" array
}, {
$group: {
"_id": "$regions.k",
"count": { $sum: "$regions.v" } //
}
})
Alternatively, if you really want to get a pipe-delimited output here is what you'd do:
db.collection.aggregate({
$project: {
"result": {
$reduce: {
"input": { $objectToArray: "$regions" },
"initialValue": { k: "", v: "" }, // start with empty strings for both key and value
"in": {
k: { $concat: [ "$$value.k", "|", "$$this.k" ] }, // concatenate existing value with "|" followed by currently looked at value for both key and value
v: { $concat: [ "$$value.v", "|", { $substr: [ "$$this.v", 0, 1000 ] } ] } // substr is needed to convert an integer field into a string
//v: { $concat: [ "$$value.v", "|", { $toString: "$$this.v" } ] } // this works from MongoDB v4.0 onwards and looks a bit cleaner
}
}
}
}
}, {
$project: { // remove the leading "|"
"result.k": { $substr: [ "$result.k", 1, -1 ] },
"result.v": { $substr: [ "$result.v", 1, -1 ] }
}
})

MongoDB: How to apply multiple $project on aggregation

Given below is a sample document from the my_collection
{
"_id":ObjectId("5b3f4e1013aad111501b34c4")
"Text":["kjsdf sdfjgnks vk ekl sdsdd."],
"Title":["lkdjf wufk"],
"List":["3412","1244","7654","8476"],
"__v":0
}
I want to aggregate over my_collection such that:
A field "totalList" is generated which would contain the count of items in the array in the field "List".
Prevent field "__v" from showing up in the result.
Change the Value of the output field "List" to a join of the array
Remove the trailing comma at the end of the string in "List"
Cut short the string generated by the 4th point to contain only the first 50 characters in the field of "List"
So I wrote this code:
db.my_collection.aggregate(
[
{
$addFields: {
totalList: { $size: "$List" } // I can achieve the 1st point with this
}
},
{ $project : {
__v:0, // I can achieve the 2nd point with this
List:
{
$reduce: // I can achieve the 3rd point with this
{
input: "$List",
initialValue: "",
in: { $concat : ["$$value", "$$this", ","] }
}
}
}
}
]
);
However, I'm getting the following error when I run the above command:
Failed to execute a script.
Error:
Assert: command failed: {
"ok" : 0,
"errmsg" : "Bad projection specification, cannot include fields or add computed fields during an exclusion projection: { __v: 0.0, List: { $reduce: { input: \"$List\", initialValue: \"\", in: { $concat: [ \"$$value\", \"$$this\", \",\" ] } } } }",
"code" : 40182,
"codeName" : "Location40182"
} : aggregate failed
_getErrorWithCode#src/mongo/shell/utils.js:25:13
doassert#src/mongo/shell/assert.js:16:14
assert.commandWorked#src/mongo/shell/assert.js:370:5
DBCollection.prototype.aggregate#src/mongo/shell/collection.js:1319:5
DBCollection.prototype.aggregate#:1:355
#(shell):1:1
Error: command failed: {
"ok" : 0,
"errmsg" : "Bad projection specification, cannot include fields or add computed fields during an exclusion projection: { __v: 0.0, List: { $reduce: { input: \"$List\", initialValue: \"\", in: { $concat: [ \"$$value\", \"$$this\", \",\" ] } } } }",
"code" : 40182,
"codeName" : "Location40182"
} : aggregate failed :
_getErrorWithCode#src/mongo/shell/utils.js:25:13
doassert#src/mongo/shell/assert.js:16:14
assert.commandWorked#src/mongo/shell/assert.js:370:5
DBCollection.prototype.aggregate#src/mongo/shell/collection.js:1319:5
DBCollection.prototype.aggregate#:1:355
#(shell):1:1
Hence, I rewrote it to:
db.my_collection.aggregate(
[
{
$addFields: {
totalList: { $size: "$List" } // Achieve the 1st point with this
}
},
{ $project : {
"Text":1,
"Title":1,
__v:{ // Achieve the 2nd point with this
$cond: {
if: { $eq: [1,1] },
then: "$$REMOVE",
else: 0
}
},
List:
{
$reduce: // Achieve the 3rd point with this
{
input: "$List",
initialValue: "",
in: { $concat : ["$$value", "$$this", ","] }
}
}
}
}
]
);
Now, I could achieve the first 3 points.
However, how can I achieve the 4th and 5th point?
You can try below aggregation
db.collection.aggregate([
{ "$addFields": {
"totalList": { "$size": "$List" },
"List": {
"$substr": [
{ "$reduce": {
"input": "$List",
"initialValue": "",
"in": { "$concat": ["$$value", ",", "$$this"] }
}},
1,
50
]
}
}},
{ "$project": { "List": 1, "Test": 1, "Title": 1, "totalList": 1 }}
])
Output
[
{
"List": "3412,1244,7654,8476",
"Title": [
"lkdjf wufk"
],
"_id": ObjectId("5b3f4e1013aad111501b34c4"),
"totalList": 4
}
]
Try it here
Please try the following aggregation. I have mentioned the changes for #4 and #5 in comments below.
db.sample.aggregate(
[
{
$addFields: {
totalList: { $size: "$List" } //This is for #1
}
},
{ $project : {
"Text":1,
"Title":1,
"totalList":1,
__v:{
$cond: {
if: { $eq: [1,1] }, //This is for #2
then: "$$REMOVE",
else: 0
}
},
List:
{
$reduce: //This is for #3
{
input: "$List",
initialValue: "",
in: {
$concat: [
"$$value",
{
$cond: {
if: { $eq: [ "$$value", "" ] }, //This is for #4
then: "",
else: ", "
}
},
"$$this"
]
}
}
}
}
},
{
$project : {
"Text" : 1,
"Title" : 1,
"__v" : 1,
"List" : { $substr: [ "$List", 0, 50 ] }, //This is for #5
"totalList":1
}
}
]
);