I tried to use an example in the official document of mongodb,
db.students3.updateMany({ },
[
{ $set: { grade: { $switch: {
branches: [
{ case: { $gte: [ "$average", 90 ] }, then: "A" },
{ case: { $gte: [ "$average", 80 ] }, then: "B" },
{ case: { $gte: [ "$average", 70 ] }, then: "C" },
{ case: { $gte: [ "$average", 60 ] }, then: "D" }
],
default: "F"
} } } }
])
Property grade is defined as Number type. I got the error, when tried to update all documents in the database students3,
CastError: Cast to Number failed for value "{ '$switch': { branches: [ [Object] ] } }" (type Object) at path "grade"
Could someone explain the error?
Thanks.
You have to $trunc first, then average field would be created.
{ $set: { average : { $trunc: [ { $avg: "$tests" }, 0 ] }, modified: "$$NOW" } }
complete update
db.collection.update({},
[
{
$set: {
average: { $trunc: [ { $avg: "$tests" }, 0 ] },
modified: "$$NOW"
}
},
{
$set: {
grade: {
$switch: {
branches: [
{ case: { $gte: [ "$average", 90 ] }, then: "A" },
{ case: { $gte: [ "$average", 80 ] }, then: "B" },
{ case: { $gte: [ "$average", 70 ] }, then: "C" },
{ case: { $gte: [ "$average", 60 ] }, then: "D" }
],
default: "F"
}
}
}
}
])
mongoplayground
Related
I have a collection and its documents look like:
{
_id: ObjectId('111111111122222222223333'),
my_array: [
{
id: ObjectId('777777777788888888889999')
name: 'foo'
},
{
id: ObjectId('77777777778888888888555')
name: 'foo2'
}
//...
]
//more attributes
}
However, some documents have my_array: [{}] (with one element which is an empty array).
How can I add conditionally a projection or remove it?
I have to add it to a mongo pipeline at the end of the query, and I want to get my_array only when it has at least one element which is not an empty object. If there's an empty object remove it.
I tried with $cond and $eq in a projection stage but it is not supported. Any suggestion to solve this?
Suppose you have documents like this with my_array field:
{ "my_array" : [ ] }
{ "my_array" : [ { "a" : 1 } ] } // #(1)
{ "my_array" : null }
{ "some_fld" : "some value" }
{ "my_array" : [ { } ] }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] } // #(2)
And, the following aggregation will filter and the result will have the two documents (1) and (2):
db.collection.aggregate([
{
$match: {
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
}
}
])
This also works with a find method:
db.collection.find({
$expr: {
$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]
}
})
To remove the my_array field, from a document when its empty, then you try this aggregation:
db.collection.aggregate([
{
$addFields: {
my_array: {
$cond: [
{$and: [
{ $eq: [ { $type: "$my_array" }, "array" ] },
{ $gt: [ { $size: "$my_array" }, 0 ] },
{ $ne: [ [{}], "$my_array" ] }
]},
"$my_array",
"$$REMOVE"
]
}
}
}
])
The result:
{ }
{ "my_array" : [ { "a" : 1 } ] }
{ }
{ "a" : 1 }
{ }
{ "my_array" : [ { "a" : 2 }, { "a" : 3 } ] }
You can't do that in a query, however in an aggregations you can add $filter to you pipeline, like so:
db.collection.aggregate([
{
$project: {
my_array: {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
}
])
Mongo Playground
However unless this is "correct" behavior I suggest you clean up your database, it's much simpler to maintain "proper" structure than to update all your queries everywhere.
You can use this update to remove these objects:
db.collection.update({
"myarray": {}
},
[
{
"$set": {
"my_array": {
$filter: {
input: "$my_array",
as: "elem",
cond: {
$ne: [
{},
"$$elem"
]
}
}
}
}
},
],
{
"multi": false,
"upsert": false
})
Mongo Playground
my collection in mongo db like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:[2,3,4],
c:3,
d:4,
e:5
},
{
a:11,
b:[22,33,44],
c:33,
d:44,
e:55
}
]
}
I want to get a result with project op to give me a specific field in an array like this:
{
name:"mehdi",
grades:
[
{
a:1,
b:2
},
{
a:11,
b:22
}
]
}
how can I do this?
You can use $map to select a,b fields using $type to determine whether it's an array or number:
db.collection.aggregate([
{
$project: {
grades: {
$map: {
input: "$grades",
in: {
a: { $cond: [ { $eq: [ { $type: "$$this.a" }, "array" ] }, { $arrayElemAt: [ "$$this.a", 0 ] }, "$$this.a" ] },
b: { $cond: [ { $eq: [ { $type: "$$this.b" }, "array" ] }, { $arrayElemAt: [ "$$this.b", 0 ] }, "$$this.b" ] },
}
}
}
}
}
])
Mongo Playground
If we imagine this kind of document structure :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
},
{
id: 4,
name: "",
values : {
a: 89,
b: 2
}
}
// ...
]
Is it possible to get one or more lists of documents where, for example, the sum of the $.values.a equals 100 and the sum of the $.values.b equals 120? Or if not is it possible to sort the bests fits with a kind of threshold?
For example, the best output can be something like that :
[
{
id: 1,
name: "",
values : {
a: 24,
b: 42
}
},
{
id: 2,
name: "",
values : {
a: 43,
b: 53
}
},
{
id: 3,
name: "",
values : {
a: 33,
b: 25
}
}
]
There is no any native implementation...
But, You can have desired results if your data meets some requirements:
You collection has no too much data (this solution scales badly)
Your id field is unique
Your collection has index for id field
Explanation
We sort by id
With $lookup with the same collection (it's important ´id´ to be indexed) and pick next 10 documents for the current document L i=(Doc i+1 ... Doc i+11)
With $reduce, we count from i ... i+n untill a > 100 and b > 120
With $facet, we separate lists which meets exactly a=100, b=120 results (equals) and threshold (+- 10 for values.a and values.b)
Last steps, if any equals exists, we ignore threshold. Otherwise, we take threshold.
db.collection.aggregate([
{
$sort: {
id: 1
}
},
{
$lookup: {
from: "collection",
let: {
id: "$id"
},
pipeline: [
{
$sort: {
id: 1
}
},
{
$match: {
$expr: {
$gt: [
"$id",
"$$id"
]
}
}
},
{
$limit: 10
}
],
as: "bucket"
}
},
{
$replaceRoot: {
newRoot: {
$reduce: {
input: "$bucket",
initialValue: {
a: "$values.a",
b: "$values.b",
data: [
{
_id: "$_id",
id: "$id",
name: "$name",
values: "$values"
}
]
},
in: {
a: {
$add: [
"$$value.a",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.a",
0
]
}
]
},
b: {
$add: [
"$$value.b",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
"$$this.values.b",
0
]
}
]
},
data: {
$concatArrays: [
"$$value.data",
{
$cond: [
{
$and: [
{
$lt: [
"$$value.a",
100
]
},
{
$lt: [
"$$value.b",
120
]
}
]
},
[
"$$this"
],
[]
]
}
]
}
}
}
}
}
},
{
$facet: {
equals: [
{
$match: {
a: 100,
b: 120
}
}
],
threshold: [
{
$match: {
a: {
$gte: 90,
$lt: 110
},
b: {
$gte: 110,
$lt: 130
}
}
}
]
}
},
{
$project: {
result: {
$cond: [
{
$gt: [
{
$size: "$equals"
},
0
]
},
"$equals",
"$threshold"
]
}
}
},
{
$unwind: "$result"
}
])
MongoPlayground
I am having issues with referencing a nested array item in a $cond statement.
db.getCollection('bookings').aggregate([
{
$lookup: {
from: "listings",
localField: "listingId",
foreignField: "_id",
as: "listing"
}
},
{
$match: {
$and: [
{
locationId: ObjectId("5c0f0c882fcf07fb08890c27")
},
{
$or: [
{
$and: [
{
state: "booked"
},
{
startDate: {
$lte: new Date()
}
},
{
startDate: {
$gte: ISODate("2019-12-18T07:00:00.000Z")
}
}
]
},
{
$and: [
{
listing: {
$elemMatch: {
inspectionStatus: "none"
}
}
},
{
endDate: {
$lte: new Date()
}
},
{
endDate: {
$gte: ISODate("2019-12-18T07:00:00.000Z")
}
},
{
state: {
$in: [
"active",
"returned"
]
}
}
]
},
{
$and: [
{
state: {
$ne: "cancelled"
}
},
{
$or: [
{
$and: [
{
startDate: {
$gte: ISODate("2019-12-20T07:00:00.993Z")
}
},
{
startDate: {
$lte: ISODate("2019-12-21T06:59:59.999Z")
}
}
]
},
{
$and: [
{
endDate: {
$gte: ISODate("2019-12-20T07:00:00.993Z")
}
},
{
endDate: {
$lte: ISODate("2019-12-21T06:59:59.999Z")
}
}
]
}
]
}
]
}
]
}
]
}
},
{
$addFields: {
isLate: {
$cond: [
{
$or: [
{
$and: [
{
$eq: [
"$listing.0.inspectionStatus",
"none"
]
},
{
$lte: [
"$endDate",
new Date()
]
},
{
$gte: [
"$endDate",
ISODate("2019-12-18T07:00:00.000Z")
]
},
{
$in: [
"$state",
[
"active",
"returned"
]
]
},
]
},
{
$and: [
{
$eq: [
"$state",
"booked"
]
},
{
$lte: [
"$startDate",
new Date()
]
},
{
$gte: [
"$startDate",
ISODate("2019-12-18T07:00:00.000Z")
]
}
]
}
]
},
true,
false
]
}
}
}
])
In the above, the following lines in the $cond statement does not work at all:
$eq: [
"$listing.0.inspectionStatus",
"none"
]
My question is - how do I make the above work? Note that there is always only one array item in the listing field after the lookup (never more than one array item in there). I've tried different variations like $listing.$0.$inspectionStatus - but nothing seems to work. I could go down the trajectory of researching group and filter - but I feel like this is overkill when I simply always want to access the first and only item in the listing array.
Please use $in keyword instead of $eq keyword in $cond keyword
db.demo1.aggregate([
{
$lookup: {
from: "demo2",
localField: "listingId",
foreignField: "_id",
as: "listing"
}
},
{
$match: {
$and: [
{
locationId: ObjectId("5c0f0c882fcf07fb08890c27")
},
{
$or: [
{
$and: [
{
state: "booked"
},
{
startDate: {
$lte: new Date()
}
},
{
startDate: {
$gte: ISODate("2019-12-18T07:00:00.000Z")
}
}
]
},
{
$and: [
{
listing: {
$elemMatch: {
inspectionStatus: "none"
}
}
},
{
endDate: {
$lte: new Date()
}
},
{
endDate: {
$gte: ISODate("2019-12-18T07:00:00.000Z")
}
},
{
state: {
$in: [
"active",
"returned"
]
}
}
]
},
{
$and: [
{
state: {
$ne: "cancelled"
}
},
{
$or: [
{
$and: [
{
startDate: {
$gte: ISODate("2019-12-20T07:00:00.993Z")
}
},
{
startDate: {
$lte: ISODate("2019-12-21T06:59:59.999Z")
}
}
]
},
{
$and: [
{
endDate: {
$gte: ISODate("2019-12-20T07:00:00.993Z")
}
},
{
endDate: {
$lte: ISODate("2019-12-21T06:59:59.999Z")
}
}
]
}
]
}
]
}
]
}
]
}
},
{
$addFields: {
isLate: {
$cond: [
{
$or: [
{
$and: [
{
$in: [
"none",
"$listing.inspectionStatus",
]
},
{
$lte: [
"$endDate",
new Date()
]
},
{
$gte: [
"$endDate",
ISODate("2019-12-18T07:00:00.000Z")
]
},
{
$in: [
"$state",
[
"active",
"returned"
]
]
},
]
},
{
$and: [
{
$eq: [
"$state",
"booked"
]
},
{
$lte: [
"$startDate",
new Date()
]
},
{
$gte: [
"$startDate",
ISODate("2019-12-18T07:00:00.000Z")
]
}
]
}
]
},
true,
false
]
}
}
}
])
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"]
}
}}
]
}
})