get a datetime from mongo and push it to the array - mongodb

I have documents with the following props:
{
'published_date': '2020/03/10 07:20:09',
'relationships': [
{'rel_name': 'HAS_REL'},
{'rel_name': 'HAS_NO_REL'},
]
}
I want to add in each field of relationships that has as rel_name the value of HAS_REL the value of the published date as the property dict. The document will become as follows:
{
'published_date': '2020/03/10 07:20:09',
'relationships': [
{ 'rel_name': 'HAS_REL'
'date': 2020,03,10,07,20,09
},
{'rel_name': 'HAS_NO_REL'},
]
}
So far my query looks something like this:
TEST_COLLECTION.update_one(
{'_id': ObjectId(document_id)},
{'$set': {'relationships.$[elem].date': {'$dateFromString': '$published_date'}}},
False,
False,
None,
[{'elem.rel_name': 'HAS_RELATIONSHIP'}],
)
But I'm getting the error:
WriteError: The dollar ($) prefixed field '$dateFromString' in 'parsed.relationships.1.date.$dateFromString' is not valid for storage.
any ideas?
UPDATE
With the initial answer, I've updated the query such as follows:
TEST_COLLECTION.update_one(
{'_id': ObjectId(document_id)},
[
{'$set': {
'relationships': {
'$let': {
'vars': {
'date': { '$dateFromString': { 'dateString': '$published_date', format: "%Y/%m/%d %H:%M:%S" } }
},
'in': {
'$map': {
'input': "$relationships",
'in': {
'$cond': {
'if': { '$eq': ["$$this.rel_name", "HAS_REL"] },
'then': { '$mergeObjects': ["$$this", { 'date': "$$date" }] },
'else': "$$this"
}
}
}
}
}
}
}
}
]
)
However, it seems I'me not getting a correct document:
InvalidDocument: documents must have only string keys, key was
built-in function format

Have a look at $dateFromString
You have to specify format field, unless you use default format "%Y-%m-%dT%H:%M:%S.%LZ". Storing date/time values as string is usually a design flaw.
So, it must be
{'$dateFromString': { dateString: '$published_date', format: "%Y/%m/%d %H:%M:%S" } }
Note, the time is considered at UTC time. Set field timezone to specify the time zone if required.
Be aware, $dateFromString() is an aggregation function, so you must use
TEST_COLLECTION.update_one(
{ '_id': ObjectId(document_id) },
[
{
$set: {
relationships: {
$map: {
input: "$relationships",
in: {
$cond: {
if: { $eq: ["$$this.rel_name", "HAS_REL"] },
then: {
$mergeObjects: [
"$$this",
{
date: {
$dateFromString: {
dateString: '$published_date',
format: "%Y/%m/%d %H:%M:%S"
}
}
}
]
},
else: "$$this"
}
}
}
}
}
}
]
)
or another style:
TEST_COLLECTION.update_one(
{ '_id': ObjectId(document_id) },
[
{
$set: {
relationships: {
$let: {
vars: {
date: { $dateFromString: { dateString: '$published_date', format: "%Y/%m/%d %H:%M:%S" } }
},
in: {
$map: {
input: "$relationships",
in: {
$cond: {
if: { $eq: ["$$this.rel_name", "HAS_REL"] },
then: { $mergeObjects: ["$$this", { date: "$$date" }] },
else: "$$this"
}
}
}
}
}
}
}
}
]
)

Related

MongoDB: Add date comparison to arrayFilters

Objects of my collection have a field, that is an array of objects with one of the field being a string date
{
citizens: [{
name: 'John'
birthday: '1993/07/13'
},
{
name: 'Sarah'
birthday: '1996/07/13'
},
{
name: 'Natalia',
birthday: '2015/07/13'
}]
}
{
citizens: [{
name: 'Leo'
birthday: '1994/02/08'
},
{
name: 'Paul'
birthday: '1934/09/13'
},
{
name: 'Rego',
birthday: '2019/01/29'
}]
}
I want to set to all the users older than 18 status 'adult'
Here is what I try to do:
users.updateMany({}, {
$set: { 'citizens.$[elem].status': 'adult' },
},
{
arrayFilters: [
{ 'elem.status': { $exists: false } },
{ $lt: [{ $toDate: 'elem.$birthday' }, 18yearsaAgoDate] }, <-- 18years don't mean much here, I actually use $$NOW
],
multi: true,
});
But I get 'unknown top level operator: $lt' error when run this. How do I supposed to use $lt in arrayFilter?
Thanks in advance!
Here's how you could do it in a simple update using the aggregation pipelined updates:
db.collection.updateMany({},
[
{
$set: {
citizens: {
$map: {
input: "$citizens",
in: {
$mergeObjects: [
{
status: {
$cond: [
{
$gt: [
{
$dateDiff: {
startDate: {
$toDate: "$$this.birthday"
},
endDate: "$$NOW",
unit: "year"
}
},
18
]
},
"adult",
"$$REMOVE"
]
}
},
"$$this"
]
}
}
}
}
}
])
Mongo Playground
I've used some version 5+ operators like $dateDiff as it makes the code cleaner, but you could still achieve the same results without them using $subtract and a constant for 18 years, like so:
{
$lt: [
{
$toDate: "$$this.birthday"
},
{
$subtract: [
"$$NOW",
567648000000// 18 years in miliseconds
]
}
]
}
Mongo Playground
This is an update using the arrayFilters syntax.
db.collection.updateMany(
{ },
{
$set: { "citizens.$[elem].status": "adult" }
},
{
arrayFilters: [ { "elem.status": { $exists: false } , "elem.birthday": { $lt: "2004/07/27" } } ]
}
)
Note the date value "2004/07/27" is the day 18 years ago (very close approximate value). And using string values in date comparison requires that the value is formatted in "YYYY/mm/dd".
It would have worked like this if your date was already in the right format. Since you need to format it, I think you should use an aggregation pipeline with a $merge stage:
db.collection.aggregate([
{$set: {
citizens: {
$map: {
input: "$citizens",
in: {$mergeObjects: [
{status: {
$cond: [
{$lt: [{$toDate: "$$this.birthday"}, 18yearsaAgoDate]},
"adult",
"$$REMOVE"
]
}
},
"$$this"
]
}
}
}
}
},
{ $merge : { into : "collection" } }
])
See how it works on the playground example

How to use substring for comparison in mongo db

Below is my aggregation
db.customers.aggregate([{
$match: {
"CDF.UTILITYTYPE.D1.G1" : "12387835"
}
}, {
$project: {
_id:0,
"CDF.UTILITYTYPE.D1.G22.NAME":1,
"CDF.UTILITYTYPE.D1.G1":1,
"CDF.UTILITYTYPE.D5.EVENT": {
$filter: {
input: "$CDF.UTILITYTYPE.D5.EVENT",
as: "item",
cond: { $eq: [ "$$item.TIME", "12-04-2018 15:46:02" ] }
}
}
}
}
]).pretty();
i am comparing TIME field here i actually want to compare "06-2022" as a substring instead of "12-04-2018 15:46:02" this whole date and time format
You never store date/time values as string, it's a design flaw. Store always proper Date objects.
Once you corrected the data type, e.g. with
{
$set: {
TIME: {
$dateFromString: {
dateString: "$TIME",
format: "%d-%m-%Y %H:%M:%S"
}
}
}
}
you can filter by using for example
cond: {
$eq: [
{ $dateTrunc: { date: "$TIME" unit: "month" } },
ISODate("2022-06-01")
]
}
or supposingly
cond: {
$eq: [
{ $dateTrunc: { date: "$TIME" unit: "month" } },
{ $dateTrunc: { date: "$$NOW" unit: "month" } }
]
}
db.customers.aggregate([{
$match: {
"CDF.UTILITYTYPE.D1.G1" : "12387835"
}
}, {
$project: {
_id:0,
"CDF.UTILITYTYPE.D1.G22.NAME":1,
"CDF.UTILITYTYPE.D1.G1":1,
"CDF.UTILITYTYPE.D5.EVENT": {
$filter: {
input: "$CDF.UTILITYTYPE.D5.EVENT",
as: "item",
cond: { $regexMatch: { input:"$$item.TIME", regex: "05-2022"}}
}
}
}
}
]).pretty();

Splitting an alphanumeric string like "3a" or "32ab" in mongodb aggregation pipeline

I would like to split an alphanumeric string like 3a into "3" and "a". Please help if any one has an idea. I can't use the $split in mongodb aggregation.
I'm not sure that this is efficient, but this answer may give you a solution.
Since we can't use regex in $split,
First stage - divide the sentence into words and store in char[]
Flat the char[] using $unwind
Categorize all string into strings[] and all numbers into numbers[] using $facet. Here we use $match with regex
Then combined as what you need.
Assume this is your string.
{
char:"32ab"
}
The mongo script might be,
db.collection.aggregate([{$addFields: {
'char': {
$map: {
input: {
$range: [
0,
{
$strLenCP: '$char'
}
]
},
'in': {
$substrCP: [
'$char',
'$$this',
1
]
}
}
}
}}, {$unwind: {
path: '$char',
preserveNullAndEmptyArrays: false
}}, {$facet: {
strings: [
{
$match: {
'char': RegExp('^[A-Za-z]+$')
}
},
{
$group: {
_id: null,
arr: {
$push: '$char'
}
}
},
{
$project: {
combined: {
$reduce: {
input: '$arr',
initialValue: '',
'in': {
$concat: [
'$$value',
'$$this'
]
}
}
}
}
}
],
numbers: [
{
$match: {
'char': {
$not: RegExp('^[A-Za-z]+$')
}
}
},
{
$group: {
_id: null,
arr: {
$push: '$char'
}
}
},
{
$project: {
combined: {
$reduce: {
input: '$arr',
initialValue: '',
'in': {
$concat: [
'$$value',
'$$this'
]
}
}
}
}
}
]
}}, {$project: {
string: {
$arrayElemAt: [
{
$ifNull: [
'$strings.combined',
''
]
},
0
]
},
number: {
$toInt:{
$arrayElemAt: [
{
$ifNull: [
'$numbers.combined',
''
]
},
0
]
}
}
}}])
And the output is
{
string : "ab",
numbers: 32
}

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

MongoDB -How to get last 30 days date from given date and that last 30 days date should be current date?

I am the beginner of MongoDB
Here I mentioned below my one document
{
"_id" : ObjectId("5e5bc292361b710c7727718e"),
"branch_id" : "BR5cc825dac42dac3aae49ff91",
"inventory" : [
{
"inventory_stock_id" : "wewe123",
"stock_name" : "xxxxx",
"stock_point" : "27",
"stock_type" : "yyyy",
"batch" : [
{
"quantity" : 40,
"manf_date" : "10-01-2020",
"exp_date" : "01-04-2020"
}
]
}
]
}
I want to get last 30 days from "exp_date" but it should be equal to current date
Here I mentioned exp_date: "01-04-2020" and the past 30 days of date is today date( "02-03-2020")
db.collection.find({"inventory.batch.exp_date" : {"$lte":"01-04-2020","$eq":"02-03-2020"}})
I don't know how to get last 30 days of exp_date and equal to current date
so anyone help me to solve this issue.
Usually it is a bad approach to store/compare Date values as strings.
You can do it like this. First convert the strings to proper Date objects:
db.collection.updateMany(
{},
[
{
$set: {
inventory: {
$map: {
input: "$inventory",
as: "inventory",
in: {
$mergeObjects: [
"$$inventory",
{
batch: {
$map: {
input: "$$inventory.batch",
in: {
quantity: "$$this.quantity",
manf_date: { $dateFromString: { dateString: "$$this.manf_date", format: "%d-%m-%Y" } },
exp_date: { $dateFromString: { dateString: "$$this.exp_date", format: "%d-%m-%Y" } }
}
}
}
}
]
}
}
}
}
}
]
)
When you have to work with Date values, then I recommend the Moment.js library.
The query would be this one:
db.collection.find(
{
"inventory.batch": {
$elemMatch: {
exp_date: {
$eq: moment().utc().add(30, 'days').startOf('day').toDate()
}
}
}
}
)
or as aggregation:
db.collection.aggregate([
{
$match: {
"inventory.batch": {
$elemMatch: {
exp_date: {
$eq: moment().utc().add(30, 'days').startOf('day').toDate()
}
}
}
}
}
])
Note, by default $dateFromString uses UTC times, whereas moment() uses your local time by default. Thus you have to use either moment().utc() or you specify the timezone field at $dateFromString.
In case you insist to keep the string values as Date, you can also use
db.collection.find(
{
"inventory.batch": {
$elemMatch: {
exp_date: {
$eq: moment().add(30, 'days').startOf('day').format("DD-MM-YYYY")
}
}
}
}
)
However, this will fail if you query with $gte, $ge, $lt, $lte operators.
Update
If you have not access to moments then you can run in purely in the aggregation:
db.collection.aggregate([
{ $unwind: "$inventory" },
{ $set: { ts: { $dateToParts: { date: { $add: ["$$NOW", { $multiply: [1000, 60, 60, 24, 30] }] } } } } },
{
$set: {
ts: {
$dateFromParts: {
year: "$ts.year",
month: "$ts.month",
day: "$ts.day",
timezone: "UTC"
}
}
}
},
{ $set: { matches: { $in: ["$ts", "$inventory.batch.exp_date"] } } },
{
$group: {
_id: { _id: "$_id", branch_id: "$branch_id" },
inventory: { $push: "$$ROOT.inventory" },
matches: { $push: "$$ROOT.matches" }
}
},
{ $match: { $expr: { $anyElementTrue: "$matches" } } },
{ $replaceRoot: { newRoot: { $mergeObjects: ["$$ROOT", "$_id"] } } },
{$unset: "matches"}
])
Or, if you like to write all on a single aggregation:
db.collection.aggregate([
{ $unwind: "$inventory" },
{
$set: {
"inventory.batch": {
$map: {
input: "$inventory.batch",
in: {
quantity: "$$this.quantity",
manf_date: { $dateFromString: { dateString: "$$this.manf_date", format: "%d-%m-%Y" } },
exp_date: { $dateFromString: { dateString: "$$this.exp_date", format: "%d-%m-%Y" } }
}
}
}
}
},
{ $set: { ts: { $dateToParts: { date: { $add: ["$$NOW", { $multiply: [1000, 60, 60, 24, 30] }] } } } } },
{
$set: {
ts: {
$dateFromParts: {
year: "$ts.year",
month: "$ts.month",
day: "$ts.day",
timezone: "UTC"
}
}
}
},
{ $set: { matches: { $in: ["$ts", "$inventory.batch.exp_date"] } } },
{
$group: {
_id: { _id: "$_id", branch_id: "$branch_id" },
inventory: { $push: "$$ROOT.inventory" },
matches: { $push: "$$ROOT.matches" }
}
},
{ $match: { $expr: { $anyElementTrue: "$matches" } } },
{ $replaceRoot: { newRoot: { $mergeObjects: ["$$ROOT", "$_id"] } } },
{ $unset: "matches" }
])