DateTime ranges with "holes" - php

Let's assume we have a "big" date and time range, like
$big = [
'start' => '2018-09-01 00:00:00',
'stop' => '2018-09-01 23:59:59'
]
I have to create an array with all ranges not included in another array, like the following:
$exclude = [
[
'start' => '2018-09-01 12:00:00',
'stop' => '2018-09-01 14:59:59'
],
[
'start' => '2018-09-01 18:00:00',
'stop' => '2018-09-01 19:59:59'
]
]
so that the resulting structure would be something like this:
$results = [
[
'start' => '2018-09-01 00:00:00',
'stop' => '2018-09-01 11:59:59'
],
[
'start' => '2018-09-01 15:00:00',
'stop' => '2018-09-01 17:59:59'
],
[
'start' => '2018-09-01 20:00:00',
'stop' => '2018-09-01 23:59:59'
]
]
tl;dr
I have a big date time range (like the ones wrote above) and I have to create a resulting one by removing one or more ranges coming from another structure. Something like extracting the work breaks from the day, thus keeping only the real working hours.
I'm using Carbon and Laravel, any native solution to this or should I parse the whole structure and make the holes on my own? I don't like to reinvent the wheel.

I don't know a lot on carbon but I know that Carbon extends the native DateTime class so the following code can be adapted to achieve your purpose:
$result=[['start'=>$big['start'],'stop'=>''],['start'=>'','stop'=>''],['start'=>'','stop'=>$big['stop']]];//you can build this with a loop if you want...
foreach($result as $k =>$value){
switch($k){
case 0:
$result[$k]['stop']=(($date=date_create($exclude[0]['start']))&&$date->sub(new dateInterval('PT1S')))?$date->format('Y-m-d H:i:s'):'';
unset($date);
break;
case 1:
$result[$k]['start']=(($date=date_create($exclude[0]['stop']))&&$date->add(new dateInterval('PT1S')))?$date->format('Y-m-d H:i:s'):'';
$result[$k]['stop']=(($date=date_create($exclude[1]['start']))&&$date->sub(new dateInterval('PT1S')))?$date->format('Y-m-d H:i:s'):'';
unset($date);
break;
case 2:
$result[$k]['start']=(($date=date_create($exclude[1]['stop']))&&$date->add(new dateInterval('PT1S')))?$date->format('Y-m-d H:i:s'):'';
unset($date);
break;
default:
break;
}
}

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'));
}
}

Calculating the relevance of a User based on Specific data

I am currently in the process of trying to form an algorithm that will calculate the relevance of a user to another user based on certain bits of data.
Unfortunately, my Maths skills have deteriorated since leaving school almost a decade ago, and as such, I am very much struggling with this. I have found an algorithm online that pushes 'hot' posts to the top of a newsfeed and figure this is a good place to start. This is the algorithm/calculation I found online (in MySQL):
LOG10(ABS(activity) + 1) * SIGN(activity) + (UNIX_TIMESTAMP(created_at) / 300000)
What I am hoping to do is adapt the above concept to work with the data and models I have in my own application. Consider this user object (trimmed down):
{
"id": 1
"first_name": "Joe",
"last_name": "Bloggs",
"counts": {
"connections": 21,
"mutual_connections": 16
},
"mutual_objects": [
{
"created_at": "2017-03-26 13:30:47"
},
{
"created_at": "2017-03-26 14:25:32"
}
],
"last_seen": "2017-03-26 14:25:32",
}
There are three bits of relevant information above that need to be considered in the algorithm:
mutual_connections
mutual_objects but taking into account that older objects should not drive up the relevance as much as newer objects, hence the created_at field.
last_seen
Can anyone suggest a fairly simple (if that's possible) way of doing this?
This was my idea, but in all honesty, I have no idea what it is doing so I cannot be sure if it is a good solution and I have also missed out last_seen as I could not find a way to add this:
$mutual_date_sum = 0;
foreach ($user->mutual_objects as $mutual_object) {
$mutual_date_sum =+ strtotime($mutual_object->created_at);
}
$mutual_date_thing = $mutual_date_sum / (300000 * count($user->mutual_objects));
$relevance = log10($user->counts->mutual_connections + 1) + $mutual_date_thing;
Just to be clear, I am not looking to implement some sort of government level AI, 50,000 line algorithm from a mathematical genius. I am merely looking for a relatively simple solution that will do the trick for the moment.
UPDATE
I have had a little play and have managed to build the following test. It seems the mutual_objects very much carries the weight in this particular algorithm as I would expect to see users 4 and 5 higher up the results list given their large number of mutual_connections.
I don't know if this makes it easier to amend/play with, but this is probably the best I can do. Please help if you have any suggestions :-)
$users = [
[
'id' => 1,
'mutual_connections' => 15,
'mutual_objects' => [
[
'created_at' => '2017-03-26 14:25:32'
],
[
'created_at' => '2017-03-26 14:25:32'
],
[
'created_at' => '2017-02-26 14:25:32'
],
[
'created_at' => '2017-03-15 14:25:32'
],
[
'created_at' => '2017-01-26 14:25:32'
],
[
'created_at' => '2017-03-26 14:25:32'
],
[
'created_at' => '2016-03-26 14:25:32'
],
[
'created_at' => '2017-03-26 14:25:32'
]
],
'last_seen' => '2017-03-01 14:25:32'
],
[
'id' => 2,
'mutual_connections' => 2,
'mutual_objects' => [
[
'created_at' => '2016-03-26 14:25:32'
],
[
'created_at' => '2015-03-26 14:25:32'
],
[
'created_at' => '2017-02-26 14:25:32'
],
[
'created_at' => '2017-03-15 14:25:32'
],
[
'created_at' => '2017-01-26 14:25:32'
],
[
'created_at' => '2017-03-26 14:25:32'
],
[
'created_at' => '2016-03-26 14:25:32'
],
[
'created_at' => '2016-03-26 14:25:32'
],
[
'created_at' => '2016-03-26 14:25:32'
],
[
'created_at' => '2017-03-15 14:25:32'
],
[
'created_at' => '2017-02-26 14:25:32'
],
[
'created_at' => '2017-03-15 14:25:32'
],
[
'created_at' => '2017-01-26 14:25:32'
],
[
'created_at' => '2017-03-12 14:25:32'
],
[
'created_at' => '2016-03-13 14:25:32'
],
[
'created_at' => '2017-03-17 14:25:32'
]
],
'last_seen' => '2015-03-25 14:25:32'
],
[
'id' => 3,
'mutual_connections' => 30,
'mutual_objects' => [
[
'created_at' => '2017-02-26 14:25:32'
],
[
'created_at' => '2017-03-26 14:25:32'
]
],
'last_seen' => '2017-03-25 14:25:32'
],
[
'id' => 4,
'mutual_connections' => 107,
'mutual_objects' => [],
'last_seen' => '2017-03-26 14:25:32'
],
[
'id' => 5,
'mutual_connections' => 500,
'mutual_objects' => [],
'last_seen' => '2017-03-26 20:25:32'
],
[
'id' => 6,
'mutual_connections' => 5,
'mutual_objects' => [
[
'created_at' => '2017-03-26 20:55:32'
],
[
'created_at' => '2017-03-25 14:25:32'
]
],
'last_seen' => '2017-03-25 14:25:32'
]
];
$relevance = [];
foreach ($users as $user) {
$mutual_date_sum = 0;
foreach ($user['mutual_objects'] as $bubble) {
$mutual_date_sum =+ strtotime($bubble['created_at']);
}
$mutual_date_thing = empty($mutual_date_sum) ? 1 : $mutual_date_sum / (300000 * count($user['mutual_objects']));
$relevance[] = [
'id' => $user['id'],
'relevance' => log10($user['mutual_connections'] + 1) + $mutual_date_thing
];
}
$relevance = collect($relevance)->sortByDesc('relevance');
print_r($relevance->values()->all());
This prints out:
Array
(
[0] => Array
(
[id] => 3
[relevance] => 2485.7219150272
)
[1] => Array
(
[id] => 6
[relevance] => 2484.8647045837
)
[2] => Array
(
[id] => 1
[relevance] => 622.26175831599
)
[3] => Array
(
[id] => 2
[relevance] => 310.84394042139
)
[4] => Array
(
[id] => 5
[relevance] => 3.6998377258672
)
[5] => Array
(
[id] => 4
[relevance] => 3.0334237554869
)
)
This problem is a candidate for machine learning. Look for an introductory book, because I think that it is not very complex and you could do it. If not, depending on the income you make with your website, you might consider hiring someone who does it for you.
If you prefer to do it "manually"; you will build your own model with specific weights to different factors. Be aware that our brains deceive us very often and what you think is a perfect model might be far from optimal.
I would suggest you to start right away storing data on which users each user interacts more with; so you can compare your results with real data. Also, in the future you will have a foundation to build a proper machine learning system.
Having said that, here is my proposal:
In the end, you want a list like this (with 3 users):
A->B: relevance
----------------
User1->User2: 0.59
User1->User3: 0.17
User2->User1: 0.78
User2->User3: 0.63
User3->User1: 0.76
User3->User2: 0.45
1) For each user
1.1) Compute and cache the age of every user's 'last_seen', in days, integer rounding down (floor).
1.2) Store max(age(last_seen)) -let's call it just max-. This is one value, not one per user. But you can only compute it once you have previously computed the age of every user
1.3) For each user, change the stored age value with the result of (max-age)/max to get a value between 0 and 1.
1.4) Compute and cache also every object's 'created_at', in days.
2) For each user, comparing with every other user
2.1) Regarding mutual connections, think of this: if A has 100 connections, 10 of them shared with B, and C has 500 connections, 10 of them shared with D, do you really take 10 as the value for the calculation in both cases? I would take the percentage. For A->B it would be 10 and for C->D it would be 2. And then /100 to have a value between 0 and 1.
2.2) Pick a maximum age for mutual objects to be relevant. Let's take 365 days.
2.3) In user A, remove objects older than 365 days. Do not really remove them, just filter them out for the sake of these calculations.
2.4) From the remaining objects, compute the percentage of mutual objects with each of the other users.
2.5) For each one of these other users, compute the average age of the objects in common from the previous step. Take the maximum age (365), subtract the computed average and /365 to have a value between 0 and 1.
2.6) Retrieve the age value of the other user.
So, for each combination of A->B, you have four values between 0 and 1:
MC: mutual connections A-B
MO: mutual objects A-B
OA: avg mutual object age A-B
BA: age of B
Now you have to assign weights to each one of them in order to find the optimal solution. Assign percentages which sum 100 to make your life easier:
Relevance = 40 * MC + 30 * MO + 10 * OA + 20 * BA
In this case, since OA is so related to MO, you can mix them:
Relevance = 40 * MC + 20 * MO + 20 * MO * OA + 20 * BA
I would suggest running this overnight, every day. There are many ways to improve and optimize the process... have fun!

Laravel 5 Eloquent sum of multiplied columns for mongo DB

This is my invoices mongo table
{
'_id': ObjectId("565d78336fe444611a8b4593"),
'TransactionID': 'X020',
'Type': 'SALESINVOICE',
'InvoiceNumber': 'ABC020',
'Date': '2015-11-01 00:00:00',
'DueDate': '2015-12-01 00:00:00',
'CurrencyCode': 'GBP',
'CurrencyRate': NumberLong(1.2),
'Total': NumberLong(200),
'PlannedPaymentDate': '',
'HasAttachments': 0,
'UpdatedDate': '2015-12-01 10:36:35',
'AmountDue': NumberLong(200),
'AmountPaid': 0,
'Status': 'OPEN',
'AmountCredited': 0,
'AmountDebited': 0,
'Source': 1,
'InvoiceID': 'csv_1_X020',
'ExpectedPaymentDate': '2015-12-01 00:00:00',
'ContactID': 1,
'ValidateStatus': 'IMPORT',
'StatusChangeDate': ''
}
What i want the multiplication of two fields (Total * CurrencyRate)
I tried this query ::
DB::connection($this->MongoSchemaName)->collection($this->InvoicesTable)->where('Type', 'PAYMENT')->where('ContactID', (int)$customer->ContactID)->select(DB::raw('sum(Total*CurrencyRate)'))->first();
and tried more ::
DB::connection($this->MongoSchemaName)->collection($this->InvoicesTable)->where('Type', 'PAYMENT')->where('ContactID', (int)$customer->ContactID)->sum(DB::raw('Total * CurrencyRate'));
but not getting the exact out put all time i get 0
I believe aggregation operators like sum expect exact column name as a parameter. You can try to project the multiplication first, then sum the result:
DB::connection($this->MongoSchemaName)
->collection($this->InvoicesTable)
->where('ContactID', (int)$customer->ContactID)
->project([
'ContactID' => 1,
'TotalInBaseCurrency' => ['$multiply' => ['$Total', '$CurrencyRate']]
])
->sum('TotalInBaseCurrency')
or use aggregation directly:
DB::connection($this->MongoSchemaName)
->collection($this->InvoicesTable)
->raw(function($collection) use ($customer){
return $collection->aggregate([
['$match' => [
'ContactID' => (int)$customer->ContactID,
'Type' => 'PAYMENT'
]
],
['$group' => [
'_id' => '$ContactID',
'TotalInBaseCurrency' => [
'$sum' => ['$multiply' => ['$Total', '$CurrencyRate']]
]
]
]
]);
})

Append nested JSON array with php

I have a nested json array in a file that looks like this:
{
"id": 12345679,
"gid": 6012,
"history": [
{
"date": "0000-00-00 00:00:00",
"rank": 6
}
]}
I am curious how I can read the json file and append the history array with php, then re-write the file named data.json.
This is what I've got thus far.
$json = file_get_contents('data.json');
$json = (array)json_decode($json);
$output = $json['history'][] = array(
array('date' => '0000-00-00 00:00:00', 'rank' => 3),
array('date' => '0000-00-00 00:00:00', 'rank' => 2),
array('date' => '0000-00-00 00:00:00', 'rank' => 6)
);
$data = json_encode(array_marge($json, $output));
Thanks for the help!
You're currently appending a block of three subarrays. What you want to do is appeand each individually:
$json['history'][] = array('date' => '0000-00-00 00:00:00', 'rank' => 3);
$json['history'][] = array('date' => '0000-00-00 00:00:00', 'rank' => 2);
$json['history'][] = array('date' => '0000-00-00 00:00:00', 'rank' => 6);
Then use that as $output.
Alternatively use array_merge for adding your three-entry list onto the input $json["history"]. But don't merge the two arrays $json and $output like you tried last.

Is this a bug in PHP's DateTime:diff?

>> $start_dt = new DateTime()
DateTime::__set_state(array(
'date' => '2012-04-11 08:34:01',
'timezone_type' => 3,
'timezone' => 'America/Los_Angeles',
))
>> $end_dt = new DateTime()
DateTime::__set_state(array(
'date' => '2012-04-11 08:34:06',
'timezone_type' => 3,
'timezone' => 'America/Los_Angeles',
))
>> $start_dt->setTimestamp(strtotime('31-Jan-2012'))
DateTime::__set_state(array(
'date' => '2012-01-31 00:00:00',
'timezone_type' => 3,
'timezone' => 'America/Los_Angeles',
))
>> $end_dt->setTimestamp(strtotime('1-Mar-2012'))
DateTime::__set_state(array(
'date' => '2012-03-01 00:00:00',
'timezone_type' => 3,
'timezone' => 'America/Los_Angeles',
))
>> $interval = $start_dt->diff($end_dt)
DateInterval::__set_state(array(
'y' => 0,
'm' => 0,
'd' => 30,
'h' => 0,
'i' => 0,
's' => 0,
'invert' => 0,
'days' => 30,
))
>> $interval->format('%mm %dd')
'0m 30d'
i.e., 31-Jan-2012 to 1-Mar-2012 yields less than a month! I'd expect the output to be 1 month, 1 day. It shouldn't matter the number of days in February; that's the point of using a time library -- it's supposed to handle these things. WolframAlpha agrees.
Should I file a bug to PHP? Is there a hack/fix/workaround to get months to work as expected?
Updated answer
This behavior of DateTime::diff is certainly unexpected, but it's not a bug. In a nutshell, diff returns years, months, days etc such that if you did
$end_ts = strtotime('+$y years +$m months +$d days' /* etc */, $start_ts);
you would get back the timestamp that corresponds to end original end date.
These additions are performed "blindly" and then date correction applies (e.g. Jan 31 + 1 month would be Feb 31, corrected to Mar 2 or Mar 3 depending on the year). In this specific example you cannot add even one month as salathe also explains.
Should I file a bug to PHP?
No.
The "month" part of the interval means that the month part of the start date can be incremented by that many months. The behaviour in PHP, taking your start date of 31-Jan-2012 and incrementing the month (literally, 31-Feb-2012) and then correcting for a valid date (PHP does this for you) would give 02-Mar-2012 which is later than the target date that you are working with.
To demonstrate this, take your start date and add n months for a few months to see the behaviour.
31-Jan-2012 (Interval)
02-Mar-2012 (P1M)
31-Mar-2012 (P2M)
01-May-2012 (P3M)
31-May-2012 (P4M)
01-Jul-2012 (P5M)
You can see that the month is being incremented, then adjusted to make a valid date.

Categories