Query to select documents based only on month or year - php

Is there a query to select documents based only on month or year in MongoDB, something like the equivalent for the following code in mysql;
$q="SELECT * FROM projects WHERE YEAR(Date) = 2011 AND MONTH(Date) = 5";
I am looking for the MongoDB equivalent, can anyone help?

Use the aggregation framework to get the query, in particular the Date Aggregation Operators $year and $month. The aggregation pipeline that gives you the above query would look like this:
var pipeline = [
{
"$project": {
"year": { "$year": "$date" },
"month": { "$month": "$date" },
"other_fields": 1
}
},
{
"$match": {
"year": 2011,
"month": 5
}
}
]
db.project.aggregate(pipeline);
The equivalent PHP query would be:
$m = new MongoClient("localhost");
$c = $m->selectDB("examples")->selectCollection("project");
$pipeline = array(
array(
'$project' => array(
"year" => array("$year" => '$date'),
"month" => array("$month" => '$date'),
"other_fields" => 1,
)
),
array(
'$match' => array(
"year" => 2011,
"month" => 5,
),
),
);
$results = $c->aggregate($pipeline);
var_dump($results);

Related

How to override an array of overlaping timeslots

So I need to create a sort of day-per-day calendar with available times, in order for a user to be able to book a meeting with one doctor from a cabinet of multiple doctors.
I hope that this explanation is not too weird already..
Btw I use Laravel 5.5
Here's an example:
Default Schedule of the cabinet : 9:00 to 19:00
Doctor 1 says that on monday, he'll be only available from 13:00 to 15:00
Doctor 2 says that on monday, he'll be only available from 10:00 to 14:00
When I query the available timeslots :
$ids = Doctor::all()->pluck('id');
$workingSchedules = WorkingSchedule::whereIn('user_id', $ids)
->orderBy('start_date')
->whereDate('start_date', '=', $this->datetime->format('Y-m-d'))
->get();
I get:
0 => [
"start_date" => "2017-09-18 10:00:00"
"end_date" => "2017-09-18 14:00:00"
]
1 => [
"start_date" => "2017-09-18 13:00:00"
"end_date" => "2017-09-18 15:00:00"
]
And if nothing shows up from the Database then I use the default cabinet hours.
Then I use Carbon diffInMinutes() method to construct an array of 30 minutes timeslots between those date range (that the user can select).
Anyway, for my script to work correcty I need to transform the result I showed you into this:
0 => [
"start_date" => "2017-09-18 10:00:00"
"end_date" => "2017-09-18 15:00:00"
]
As I only have two timeslots in this example it might be simple a solution, but I might also get an array of 10 timeslots that overlapse one another..
Can somebody help me find a elegant solution that will cover all possible case ?
Thanks a lot in advance.
To be easier, I will suppose $workingSchedules is an array of numbers, then we can easily compare elements
$workingSchedules = [
[
'start_date' => 1,
'end_date' => 5,
],
[
'start_date' => 13,
'end_date' => 16,
],
[
'start_date' => 16,
'end_date' => 17,
],
];
$result = [$workingSchedules[0]];
$index = 0;
foreach ($workingSchedules as $row) {
if ($result[$index]['end_date'] >= $row['start_date']) {
$result[$index]['end_date'] = max($result[$index]['end_date'], $row['end_date']);
} else {
$index++;
$result[] = $row;
}
}
var_dump($result);
Above code will print:
[
[
'start_date' => 1,
'end_date' => 5,
],
[
'start_date' => 13,
'end_date' => 17,
],
]
You can custom the code to compare 2 dates instead numbers
If $workingSchedules is empty, we can simply return default schedule
To merge overlapping time-periods, you could use this code:
$result = [];
$i = -1;
foreach ($workingSchedules as $row) {
if ($i < 0 || $row["end_date"] > $result[$i]["end_date"]) {
if ($i >= 0 && $row["start_date"] <= $result[$i]["end_date"]) {
$result[$i]["end_date"] = $row["end_date"];
} else {
$result[++$i] = $row;
}
}
}
$result will then have non-overlapping periods only.
I hope this will help.
$workingSchedules=array(
0=>array(
"start_date" => "2017-09-18 10:00:00",
"end_date" => "2017-09-18 14:00:00"),
1=>array(
"start_date" => "2017-09-18 13:00:00",
"end_date" => "2017-09-18 15:00:00"
)
);
foreach ($workingSchedules as $schedule){
$start=new DateTime($schedule['start_date']);
$end=new DateTime($schedule['end_date']);
while ($start<=$end){
echo $start->format('Y-m-d H:i')."<br/>";
$start=$start->add(new DateInterval('PT'.'30'.'M'));
}
}

Aggregate nested array in Mongodb

I have a mongo collection lie this :
{
"_id":ObjectId("55f16650e3cf2242a79656d1"),
"user_id":11,
"push":[
ISODate("2015-09-08T11:14:18.285 Z"),
ISODate("2015-09-08T11:14:18.285 Z"),
ISODate("2015-09-09T11:14:18.285 Z"),
ISODate("2015-09-10T11:14:18.285 Z"),
ISODate("2015-09-10T11:14:18.285 Z")
]
}{
"_id":ObjectId("55f15c78e3cf2242a79656c3"),
"user_id":12,
"push":[
ISODate("2015-09-06T11:14:18.285 Z"),
ISODate("2015-09-05T11:14:18.285 Z"),
ISODate("2015-09-07T11:14:18.285 Z"),
ISODate("2015-09-09T11:14:18.285 Z"),
ISODate("2015-09-09T11:14:18.285 Z"),
ISODate("2015-09-10T11:14:18.285 Z"),
ISODate("2015-09-11T11:14:18.285 Z")
]
}
How can I find user_ids where count of timeStamps < 3 and having date(timestamp) > (currentDate-5) in single query. I will be using php and dont want to bring all the documents in memory.
Explanation:
user_id : date : count
11 : 2015-09-08 : 2
2015-09-09 : 1
2015-09-10 : 2
12 : 2015-09-05 : 1
2015-09-06 : 1
2015-09-07 : 1
2015-09-09 : 2
2015-09-10 : 1
2015-09-11 : 1
If date set to 2015-09-09(user input) it will give 3(count) for user_id 11 and 4(count) for user_id 12. So suppose count is set to 3(user input). The query should return 11(user_id). If count is set to 2, there will be no user_id available and if count is set to 5, it should return both 11 and 12
To solve this you need an aggregation pipeline that first "filters" the results to the "last 5 days" and then essentially "sums the count" of array items present in each qualifying document to then see if the "total" is "less than three".
The $size operator of MongoDB aggregation really helps here, as does $map and some additional filtering via $setDifference for the false results returned from $map, as doing this "in document first" and "within" the $group stage required, is the most efficient way to process this
$result = $collection->aggregate(array(
array( '$match' => array(
'push' => array(
'time' => array(
'$gte' => MongoDate( strtotime('-5 days',time()) )
)
)
)),
array( '$group' => array(
'_id' => '$user_id',
'count' => array(
'$sum' => array(
'$size' => array(
'$setDifference' => array(
array( '$map' => array(
'input' => '$push',
'as' => 'time',
'in' => array(
'$cond' => array(
array( '$gte' => array(
'$$time',
MongoDate( strtotime('-5 days',time()) )
)),
'$time',
FALSE
)
)
)),
array(FALSE)
)
)
)
)
)),
array( '$match' => array(
'count' => array( '$lt' => 3 )
))
));
So the after all of the work to first find the "possible" documents that contain array entries meeting the criteria via $match and then find the "total" size of the matched array items under $group, then the final $match excludes all results that are less than three in total size.
For the largely "JavaScript brains" out there ( like myself, well trained into it ) this is basically this contruct:
db.collection.aggregate([
{ "$match": {
"push": {
"$gte": new Date( new Date().valueOf() - ( 5 * 1000 * 60 * 60 * 24 ))
}
}},
{ "$group": {
"_id": "$user_id",
"count": {
"$sum": {
"$size": {
"$setDifference": [
{ "$map": {
"input": "$push",
"as": "time",
"in": {
"$cond": [
{ "$gte": [
"$$time",
new Date(
new Date().valueOf() -
( 5 * 1000 * 60 * 60 * 24 )
)
]},
"$$time",
false
]
}
}},
[false]
]
}
}
}
}},
{ "$match": { "count": { "$lt": 3 } } }
])
Also, future versions of MongoDB will offer $filter, which simplifies the whole $map and $setDifference statement part:
db.collection.aggregate([
{ "$match": {
"push": {
"$gte": new Date( new Date().valueOf() - ( 5 * 1000 * 60 * 60 * 24 ))
}
}},
{ "$group": {
"_id": "$user_id",
"count": {
"$sum": {
"$size": {
"$filter": {
"input": "$push",
"as": "time",
"cond": {
"$gte": [
"$$time",
new Date(
new Date().valueOf() -
( 5 * 1000 * 60 * 60 * 24 )
)
]
}
}
}
}
}
}},
{ "$match": { "count": { "$lt": 3 } } }
])
As well as noting that the "dates" are probably best calculated "before" the pipeline definition as a separate variable for the best accuracy.

mongodb date aggregation operators timezone adjustments with php

I'm trying to adjust the timezone with date aggregation operators.
I need to make -7 hours adjustment on the $signs.timestamp field.
This is my code:
function statsSignatures() {
$cursor = $this->db->collection->users->aggregate(
array('$unwind' => '$signs'),
array('$project'=>array(
'signs'=>'$signs',
'y'=>array('$year'=>'$signs.timestamp'),
'm'=>array('$month'=>'$signs.timestamp'),
'd'=>array('$dayOfMonth'=>'$signs.timestamp'),
'h'=>array('$hour'=>'$signs.timestamp')
)),
array('$group'=>array(
'_id'=>array('year'=>'$y','month'=>'$m','day'=>'$d','hour'=>'$h'),
'total'=>array('$sum'=>1)
)),
array('$sort'=>array(
'_id.year'=>1,
'_id.month'=>1,
'_id.day'=>1,
'_id.hour'=>1
))
);
return $cursor['result'];
}
I'm using MongoDB version 2.6.3.
Thank you a lot !
You can use $project with $subtract operator to make a -7 hour adjustment to a Date field:
{
$project : {
ts : { $subtract : [ "$signs.timestamp", 25200000 ] }
}
}
// 25200000 == 1000 milis x 60 sec x 60 mins x 7 h
The projected field ts is a Date that's offset by -7 hours.
Edit
This is the correct PHP syntax when using $subtract.
array(
'$project' => array(
'ts' => array('$subtract' => array('$signs.timestamp', 25200000))
)
)
Subtract accepts an array of values, not a key=>value pair.
I'm not sure why, but I'm getting "exception: invalid operator '$signs.timestamp'" if i'm trying to subtract this in php like this code:
$cursor = $app['mdb']->changemi->users->aggregate(
array('$unwind' => '$signs'),
array('$project' => array(
'ts'=>array('$subtract'=>array(
'$signs.timestamp'=> 25200000
))
)),
array('$project'=>array(
'y'=>array('$year'=>'$ts'),
'm'=>array('$month'=>'$ts'),
'd'=>array('$dayOfMonth'=>'$ts'),
'h'=>array('$hour'=>'$ts')
)),
array('$group'=>array(
'_id'=>array('year'=>'$y','month'=>'$m','day'=>'$d','hour'=>'$h'),
'total'=>array('$sum'=>1)
)),
array('$sort'=>array(
'_id.year'=>1,
'_id.month'=>1,
'_id.day'=>1,
'_id.hour'=>1
))
);
So I came with 2 workarounds:
backend php. json_decode
$cursor = $app['mdb']->changemi->users->aggregate(
array('$unwind' => '$signs'),
json_decode('{"$project" : {"ts" : { "$subtract" : [ "$signs.timestamp", 25200000 ] }}}',true),
array('$project'=>array(
'y'=>array('$year'=>'$ts'),
'm'=>array('$month'=>'$ts'),
'd'=>array('$dayOfMonth'=>'$ts'),
'h'=>array('$hour'=>'$ts')
)),
array('$group'=>array(
'_id'=>array('year'=>'$y','month'=>'$m','day'=>'$d','hour'=>'$h'),
'total'=>array('$sum'=>1)
)),
array('$sort'=>array(
'_id.year'=>1,
'_id.month'=>1,
'_id.day'=>1,
'_id.hour'=>1
))
);
frontend javascript (minusHours)
Date.prototype.minusHours= function(h){
this.setHours(this.getHours()-h);
return this;
}
...
"date": new Date({{ i._id.year }}, {{ i._id.month -1 }}, {{ i._id.day }}, {{ i._id.hour }}, 0, 0, 0).minusHours(7),
Here is what worked for me. Instead of doing the timezone conversion in the 'project', I just convert the timestamp while grouping.
group._id = {
year: { $year : [{ $subtract: [ "$timestamp", 25200000 ]}] },
month: { $month : [{ $subtract: [ "$timestamp", 25200000 ]}] },
day: { $dayOfMonth : [{ $subtract: [ "$timestamp", 25200000 ]}] }
};
group.count = {
$sum : 1
};
There's no need to close some objects in array, this way worked for me:
group._id = {
year: { $year : { $subtract: [ "$timestamp", 25200000 ]}},
month: { $month : { $subtract: [ "$timestamp", 25200000 ]}},
day: { $dayOfMonth : { $subtract: [ "$timestamp", 25200000 ]}}
};
group.count = {
$sum : 1
};

Equivalent of mysql max and min functions in mongo?

How can i write the below query in mongo?
select max(priority) as max, min(priority) as min from queue group by user
I'll highly appreciate if you provide a solution in PHP.
Thank you
Queries like this are performed with the aggregation framwework and the .aggregate() method. They use a $group pipeline stage with the $min and $max operators.
db.collection.aggregate([
{ "$group": {
"_id": "$user",
"max": { "$max": "$priority" },
"min": { "$min": "$priority" }
}}
])
Or more to PHP syntax:
$collection->aggregate(array(
array(
'$group' => array (
'_id' => '$user',
'max' => array( '$max' => '$priority' ),
'min' => array( '$min' => '$priority' )
)
)
));
Also see the SQL to Aggregation Mapping Chart in the documentation

How to use "group" in MongoDB with PHP?

I'm using PHP with MongoDB, How can apply below commend inside?
db.event.group({
keyf: function(doc) {
return {
year: doc.created.getFullYear(),
month: doc.created.getMonth() + 1,
day: doc.created.getDate()
}
},
reduce: function(curr, result){
result.count++;
},
initial: {count: 0}
});
I have tried below, but NOT working. Looks like not supprt keyf?
$keyf = 'function(doc){return {year: doc.created.getFullYear(), month: doc.created.getMonth()+1, day: doc.created.getDate()}}';
$initial = array('count' => 0);
$reduce = 'function(curr, result){result.count++;}';
$collection->group($keyf, $initial, $reduce);
It looks like you are basically counting the amount of documents under a date.
It should be noted that the group command has numerous flaws including:
Not officially supporting sharding (warning not to use it)
Is basically JavaScript
Is Basically a Map Reduce
Is extremely slow
that means it has since been "deprecated" in favour of the aggregation framework, which in PHP for you would be:
$db->collection->aggregate(array(
array('$group' => array(
'_id' => array(
'day' => array('$dayOfMonth' => '$created'),
'month' => array('$month' => '$created'),
'year' => array('$year' => '$created')
),
'count' => array('$sum' => 1)
))
));
To understand what operators I used etc you can look here:
http://docs.mongodb.org/manual/reference/operator/aggregation/dayOfMonth/
http://docs.mongodb.org/manual/reference/operator/aggregation/month/#exp._S_month
http://docs.mongodb.org/manual/reference/operator/aggregation/year/
http://docs.mongodb.org/manual/reference/operator/aggregation/sum/
The PHP driver does have the MongoCode class for constructing the JavaScript values that are required.
But you are actually better off using the .aggregate() command to this as it is "native* code and does not rely on the JavaScript engine. So it is much faster at producing results.
db.collection.aggregate([
{ "$group": {
"_id": {
"year": { "$year": "$created" },
"month": { "$month": "$created" },
"day": { "$dayOfMonth": "$created" }
},
"count": { "$sum": 1 }
}}
])
Data Problem
So the aggregate function works are expected, but you seem to have a problem with your test data. Here is cwhat you gave:
db.post.insert({'field':'b', 'created':new Date('2014, 1, 1')});
db.post.insert({'field':'c', 'created':new Date('2014, 1, 1 11:11:11')});
db.post.insert({'field':'d', 'created':new Date('2014, 1, 1 12:00:00')});
db.post.insert({'field':'a', 'created':new Date('2014, 1, 2')});
db.post.insert({'field':'b', 'created':new Date('2014, 1, 2')})
And this produces the data:
{ "field" : "a", "created" : ISODate("2013-12-31T13:00:00Z") }
{ "field" : "b", "created" : ISODate("2013-12-31T13:00:00Z") }
{ "field" : "c", "created" : ISODate("2014-01-01T00:11:11Z") }
{ "field" : "d", "created" : ISODate("2014-01-01T01:00:00Z") }
{ "field" : "a", "created" : ISODate("2014-01-01T13:00:00Z") }
{ "field" : "b", "created" : ISODate("2014-01-01T13:00:00Z") }
So it looks like you were trying to add "hours" in the same day to test the grouping. But the arguments to Date() are not correct. You wanted this:
db.post.insert({'field':'b', 'created':new Date('2014-01-01')});
db.post.insert({'field':'c', 'created':new Date('2014-01-01 11:11:11')});
So the whole date as a string and not the "comma" separated values

Categories