How to convert time string to ISO format in mongodb aggregation? - mongodb

All document in my collection are same as this:
{
"_id": {
"$oid": "6396c58284bfad036f960288"
},
"title": "This is a nice title.",
"time": "3266 sec"
}
But I need to convert time field like this:
{
"_id": {
"$oid": "6396c58284bfad036f960288"
},
"title": "This is a nice title.",
"time": "PT3266S"
}

$set - Set the time field. With $regexFind to capture the matched group.
$set - Set the time field. With $concat to concat string with "PT", the first element of time.captures array and "S".
db.collection.aggregate([
{
$set: {
time: {
$regexFind: {
input: "$time",
regex: "(\\d+) sec"
}
}
}
},
{
$set: {
time: {
$concat: [
"PT",
{
$arrayElemAt: [
"$time.captures",
0
]
},
"S"
]
}
}
}
])
Demo # Mongo Playground
Or you can combine both $set stages into one:
db.collection.aggregate([
{
$set: {
time: {
$concat: [
"PT",
{
$arrayElemAt: [
{
$getField: {
field: "captures",
input: {
$regexFind: {
input: "$time",
regex: "(\\d+) sec"
}
}
}
},
0
]
},
"S"
]
}
}
}
])

Related

MongoDB - Best way to choose a document based on some condition inside switch case

I have a structure where I want to match the value of a field on root level with the value of a field inside another object in the same document and then choose a single document based on some condition from the result. This is the structure I have:
{
"name": "somename",
"level": "123",
"nested":[
{
"somefield": "test",
"file": {
level:"123"
}
},
{
"somefield": "test2",
"file": {
level:"124"
}
},
{
"somefield": "test3",
"file": {
level:"123"
}
}
]
}
After unwinding and matching it on a condition (level = nested.file.level) I have left with 2 documents:
[
{
"level": "123",
"name": "somename",
"nested": {
"file": {
"level": "123"
},
"somefield": "test"
}
},
{
"level": "123",
"name": "somename",
"nested": {
"file": {
"level": "123"
},
"somefield": "test3"
}
}
]
Now I want to match on somefield values, this field has 10 different values, these values are in order so if I find a matching document then I will return it or I will go to the next value in the order and check if "somefield": "orderedValue" and so on. So for example:
test
test2
test3
test4
test5
is the order and if I find a document with has "somefield": "test" I will only return that document, else I will check for "somefield": "test2" and so on until I find a single document which satisfies my condition. This is done in order so the first to satisfy the condition that the document I want.
I want to get only 1 document in the end as a result. I thought it would be best to use $switch here and wrote a project stage with $switch.
$project: {
setting: {
$switch: {
branches: [{
'case': {
$eq: [
'$nested.somefield',
'test'
]
},
then: '$nested'
},
{
'case': {
$eq: [
'$nested.somefield',
'test2'
]
},
then: '$nested'
},
{
'case': {
$eq: [
'$nested.somefield',
'test3'
]
},
then: '$nested'
}
],
'default': 'Did not match'
}
}
}
But this won't work as this would be applied on each document and if I have 5 documents with 5 of these values then it will match all of them and return the same array of documents. Any idea on how we can return only the document which matched first?
Solution 1: With $switch operator
3rd stage: $set - Create min field and assign the value based on $switch operator.
4th stage: $sort - Order by min ascending.
5th stage: $limit - Limit to 1 document.
6th stage: $group - Group by $_id. Set the setting field by taking the first document/value with the conditions:
If the min is lesser than or equal to 5, take nested value.
Else, get "Did not match" value.
db.collection.aggregate([
{
$unwind: "$nested"
},
{
$match: {
$expr: {
$eq: [
"$nested.file.level",
"$level"
]
}
}
},
{
$set: {
min: {
$switch: {
branches: [
{
"case": {
$eq: [
"$nested.somefield",
"test"
]
},
then: 1
},
{
"case": {
$eq: [
"$nested.somefield",
"test2"
]
},
then: 2
},
{
"case": {
$eq: [
"$nested.somefield",
"test3"
]
},
then: 3
},
{
"case": {
$eq: [
"$nested.somefield",
"test4"
]
},
then: 4
},
{
"case": {
$eq: [
"$nested.somefield",
"test5"
]
},
then: 5
}
],
"default": 100
}
}
}
},
{
$sort: {
min: 1
}
},
{
$limit: 1
},
{
$group: {
_id: "$_id",
setting: {
$first: {
$cond: {
if: {
$lte: [
"$min",
5
]
},
then: "$nested",
else: "Did not match"
}
}
}
}
}
])
Demo Solution 1 # Mongo Playground
Solution 2: With $let operator
3rd stage: $set - Create min field. Declare the index variable via $let with get the array index by nested.somefield.
4th stage: $sort - Order by min ascending.
5th stage: $limit - Limit to 1 document.
6th stage: $group - Group by $_id. Set the setting field by taking the first document/value with the conditions:
If the min is greater than or equal to 0, take nested value.
Else, get "Did not match" value.
db.collection.aggregate([
{
$unwind: "$nested"
},
{
$match: {
$expr: {
$eq: [
"$nested.file.level",
"$level"
]
}
}
},
{
$set: {
min: {
$let: {
vars: {
index: {
$indexOfArray: [
[
"test",
"test2",
"test3",
"test4",
"test5"
],
"$nested.somefield"
]
}
},
in: {
$cond: {
if: {
$gt: [
"$$index",
-1
]
},
then: "$$index",
else: 100
}
}
}
}
}
},
{
$sort: {
min: 1
}
},
{
$limit: 1
},
{
$group: {
_id: "$_id",
setting: {
$first: {
$cond: {
if: {
$lte: [
"$min",
4
]
},
then: "$nested",
else: "Did not match"
}
}
}
}
}
])
Demo Solution 2 # Mongo Playground

MongoDB addFields based on condition (array of array)

Let's say I have a sample object as below
[
{
"status": "ACTIVE",
"person": [
{
"name": "Jim",
"age": "2",
"qualification": [
{
"type": "education",
"degree": {
"name": "bachelor",
"year": "2022"
}
},
{
"type": "certification",
"degree": {
"name": "aws",
"year": "2021"
}
}
]
}
]
}
]
Now, I need to add a field "score" only when the qualification.type == bachelor
This is the query I tried but could not get the proper result. Not sure what mistake I am doing. Any help is highly appreciated. Thank you in advance.
db.collection.aggregate([
{
$addFields: {
"person.qualification.score": {
$reduce: {
input: "$person.qualification",
initialValue: "",
in: {
$cond: [
{
"$eq": [
"$$this.type",
"bachelor"
]
},
"80",
"$$REMOVE"
]
}
}
}
}
}
])
$set - Set person array field.
1.1. $map - Iterate person array and return a new array.
1.1.1. $mergeObjects - Merge current iterate (person) document and the document with qualification.
1.1.1.1. $map - Iterate the qualification array and return a new array.
1.1.1.1.1. $cond - Match type of current iterate qualification document. If true, merge current iterate qualification document and the document with score field via $mergeObjects. Else remain the existing document.
db.collection.aggregate([
{
$set: {
person: {
$map: {
input: "$person",
as: "p",
in: {
$mergeObjects: [
"$$p",
{
qualification: {
$map: {
input: "$$p.qualification",
in: {
$cond: {
if: {
$eq: [
"$$this.type",
"education"
]
},
then: {
$mergeObjects: [
"$$this",
{
score: "80"
}
]
},
else: "$$this"
}
}
}
}
}
]
}
}
}
}
}
])
Sample Mongo Playground

Project one field from specific object element in an array mongodb

I have a collection:
{
"_id": 1,
"_deleted": false,
"customFields": [{
"fieldName": "sapID",
"value": ""
}, {
"fieldName": "salesTerritory",
"value": ""
}, {
"fieldName": "clientType",
"value": "Corporate"
}],
}
How can I project(aggregate) only the value field of the element with fieldName = "clientType":
db.collection.aggregate(
{
$project:{value:<code>}
}
)
I tried $filter but it does not work
db.collection.aggregate([
{
$project: {
"customFields.value": 1
}
}
])
mongoplayground
db.collection.aggregate([
{
$set: {
customFields: {
$map: {
input: "$customFields",
as: "c",
in: {
$cond: {
if: { "$eq": [ "$$c.fieldName", "clientType" ] },
then: { value: "$$c.value" },
else: "$$c"
}
}
}
}
}
}
])
mongoplayground
What about this?
db.collection.aggregate([
{
$project: {
customFields: {
$filter: {
input: "$customFields",
$cond: { $eq: ["$$this.fieldName", "clientType"] }
}
}
}
}
])
Mongo Playground

MongoDB aggregation : Keep value from previous document if null or not exists

I have a collection that contains time-based metrics. I only store them if they change over time and I want to keep their previous value in the aggregation result.
Here's an extract of the collection :
{
"_id": ObjectId("6115150f01d7d0426bcd0390"),
"conf": "conference123",
"uid": "2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"log": [
{
"dt": ISODate("2021-08-12T12:33:49.782Z"),
"connection_quality": 60,
"video_bitrate": 150
},
{
"dt": ISODate("2021-08-12T12:34:19.781Z"),
"video_bitrate": 145
// connection_quality didn't change so it's not stored
},
{
"dt": ISODate("2021-08-12T12:34:30.781Z"),
"video_bitrate": 130
// connection_quality didn't change so it's not stored
},
{
"dt": ISODate("2021-08-12T12:34:49.787Z"),
"connection_quality": 100,
"video_bitrate": 150
},
{
"dt": ISODate("2021-08-12T12:35:19.789Z"),
"video_bitrate": 160
// connection_quality didn't change so it's not stored
}
]
}
I tried the following aggregation but I don't know what to put after the last stage :
[{
$match: {
conf: 'conference123',
uid: '2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642'
}
}, {
$unwind: {
path: '$log'
}
}, {
$project: {
_id: 0,
"Date": '$log.dt',
'User ID': '$uid',
'Connection Quality': "$log.cq"
}
}]
Here's the result that I get
[
{
"Date": ISODate("2021-08-12T12:33:49.782Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":60
},
{
"Date": ISODate("2021-08-12T12:34:19.781Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642"
},
{
"Date": ISODate("2021-08-12T12:34:30.781Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642"
},
{
"Date": ISODate("2021-08-12T12:34:49.787Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":100
},
{
"Date": ISODate("2021-08-12T12:35:19.789Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642"
}
]
But this is what I want to display
[
{
"Date": ISODate("2021-08-12T12:33:49.782Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":60
},
{
"Date": ISODate("2021-08-12T12:34:19.781Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":60
},
{
"Date": ISODate("2021-08-12T12:34:30.781Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":60
},
{
"Date": ISODate("2021-08-12T12:34:49.787Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":100
},
{
"Date": ISODate("2021-08-12T12:35:19.789Z"),
"User ID":"2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642",
"Connection Quality":100
}
]
Any help would be greatly appreciated, thanks !
There is no straight way to do this operation,
$map to iterate loop of log array, check condition if connection_quality type is missing then go to select previous connection_quality otherwise return the current object
$filter to iterate loop of log and by conditions are: dt should less than and connection_quality should not missing
now we have to select the latest connection_quality from above filtered result so using $last we will select last object
$let to declare a variable and do above filter operation and return just connection_quality value
$unwind to deconstruct the log array
$project to project the result as per your requirement
db.collection.aggregate([
{
$match: {
conf: "conference123",
uid: "2dd8b4e3-9dcd-4da6-bc36-aa0988dc9642"
}
},
{
$addFields: {
log: {
$map: {
input: "$log",
as: "l",
in: {
$cond: [
{ $eq: [{ $type: "$$l.connection_quality" }, "missing"] },
{
dt: "$$l.dt",
connection_quality: {
$let: {
vars: {
log: {
$last: {
$filter: {
input: "$log",
cond: {
$and: [
{ $lt: ["$$this.dt", "$$l.dt"] },
{
$ne: [{ $type: "$$this.connection_quality" }, "missing"]
}
]
}
}
}
}
},
in: "$$log.connection_quality"
}
}
},
"$$l"
]
}
}
}
}
},
{ $unwind: "$log" },
{
$project: {
_id: 0,
"Date": "$log.dt",
"User ID": "$uid",
"Connection Quality": "$log.connection_quality"
}
}
])
Playground
You can do it with reduce
The bellow query adds the connection_quality if null or missing with the
value of the previous member that had connection_quality
It starts with a default 60, for example if the first member didn't had also
db.collection.aggregate([
{
"$addFields": {
"log": {
"$arrayElemAt": [
{
"$reduce": {
"input": "$log",
"initialValue": [
60,
[]
],
"in": {
"$let": {
"vars": {
"prv_value_logs": "$$value",
"log": "$$this"
},
"in": {
"$let": {
"vars": {
"prv_value": {
"$arrayElemAt": [
"$$prv_value_logs",
0
]
},
"logs": {
"$arrayElemAt": [
"$$prv_value_logs",
1
]
}
},
"in": {
"$cond": [
{
"$and": [
{
"$ne": [
"$$log.connection_quality",
null
]
},
{
"$ne": [
{
"$type": "$$log.connection_quality"
},
"missing"
]
}
]
},
[
"$$log.connection_quality",
{
"$concatArrays": [
"$$logs",
[
"$$log"
]
]
}
],
[
"$$prv_value",
{
"$concatArrays": [
"$$logs",
[
{
"$mergeObjects": [
"$$log",
{
"connection_quality": "$$prv_value"
}
]
}
]
]
}
]
]
}
}
}
}
}
}
},
1
]
}
}
}
])
Test code here
It doesn't change the document, just adds the missing connection_quality, if you want to change it after, you can add more stages.
Solution is fast for arrays <500 members.
Edit1
The slow part is not the $reduce its the $concat because MongodDB
doesn't have a way to add 1 element to the end fast.
Its not how many arrays you have, but how big they are.
I was curious why you said you cant use reduce and map/filter worked for you(because looks like O(n^2)), so i did a benchmark.
1000 elements (the log)
"Elapsed time: 44.408292 msecs" //reduce
"Elapsed time: 167.653179 msecs" //map and filter 3x
5000 elements
"Elapsed time: 263.549371 msecs"
"Elapsed time: 3298.880892 msecs" //10x+
10000 elements
"Elapsed time: 996.340296 msecs"
"Elapsed time: 14765.732331 msecs" //10x+
This is only for 1 document collection, so both solutions are very slow, not usable for big collections with big arrays > 500 elements.

Modify a field of all documents by appending time in the 'hh: mm A' format

These are the documents I have inside a collection:
[
{
"unix_date": 1582133934,
"text": "mongo"
},
{
"unix_date": 1580068560,
"text": "some"
},
]
I want to change the text field of all documents so that they look this way:
[
{
"unix_date": 1582133934,
"text": "mongo 12:00 PM"
},
{
"unix_date": 1580068560,
"text": "some 3:00 PM"
},
]
Note that I used random times.
This is what I tried:
db.collection.update({}, [{
$set: {
text: {
$concat: ["$text", new Date("$unix_date" * 1000).toString()]
}
}
}], {
multi: true
})
this is appending invalid date to the text field and even if it does append the correct string how can I format it to hh: mm AM/PM. Is this possible without using any external libraries? I want to do this directly inside the shell.
The reason it's failing is cause you can't execute .Js logic in mongo query like that, try as below :
db.collection.update(
{},
[
{
$set: {
text: {
$concat: ["$text", " ", {
$let: {
vars: {
hourMins: { $dateToString: { format: "%H:%M",date: { $toDate: { $multiply: ["$unix_date",1000]}},timezone: "America/Chicago"}},
hour: { $hour: { date: { $toDate: { $multiply: [ "$unix_date", 1000 ] } }, timezone: "America/Chicago" } } },
in: { $concat: [ "$$hourMins", " ", { $cond: [ { $lte: [ "$$hour", 12 ]}, "AM", "PM" ] } ] } }
}]
}
}
}
],
{
multi: true,
}
);
Ref : aggregation-pipeline
Test : mongoplayground