I'm having a hell of a time trying to solve the following problem:
It's a calendar program where given a set of available datetime sets from multiple people, I need to figure out what datetime ranges everyone is available in PHP
Availability Sets:
p1: start: "2016-04-30 12:00", end: "2016-05-01 03:00"
p2: start: "2016-04-30 03:00", end: "2016-05-01 03:00"
p3: start: "2016-04-30 03:00", end: "2016-04-30 13:31"
start: "2016-04-30 15:26", end: "2016-05-01 03:00"
I'm looking for a function that I can call that will tell me what datetime ranges all (p) people are available at the same time.
In the above example the answer should be:
2016-04-30 12:00 -> 2016-04-30 13:31
2016-04-30 15:26 -> 2016-05-01 03:00
I did find this similar question and answer
Datetime -Determine whether multiple(n) datetime ranges overlap each other in R
But I have no idea what language that is, and have to unable to translate the logic in the answer.
Well that was fun. There's probably a more elegant way of doing this than looping over every minute, but I don't know if PHP is the language for it. Note that this currently needs to manage the start and end times to search separately, although it would be fairly trivial to calculate them based on the available shifts.
<?php
$availability = [
'Alex' => [
[
'start' => new DateTime('2016-04-30 12:00'),
'end' => new DateTime('2016-05-01 03:00'),
],
],
'Ben' => [
[
'start' => new DateTime('2016-04-30 03:00'),
'end' => new DateTime('2016-05-01 03:00'),
],
],
'Chris' => [
[
'start' => new DateTime('2016-04-30 03:00'),
'end' => new DateTime('2016-04-30 13:31')
],
[
'start' => new DateTime('2016-04-30 15:26'),
'end' => new DateTime('2016-05-01 03:00')
],
],
];
$start = new DateTime('2016-04-30 00:00');
$end = new DateTime('2016-05-01 23:59');
$tick = DateInterval::createFromDateString('1 minute');
$period = new DatePeriod($start, $tick, $end);
$overlaps = [];
$overlapStart = $overlapUntil = null;
foreach ($period as $minute)
{
$peopleAvailable = 0;
// Find out how many people are available for the current minute
foreach ($availability as $name => $shifts)
{
foreach ($shifts as $shift)
{
if ($shift['start'] <= $minute && $shift['end'] >= $minute)
{
// If any shift matches, this person is available
$peopleAvailable++;
break;
}
}
}
// If everyone is available...
if ($peopleAvailable == count($availability))
{
// ... either start a new period...
if (!$overlapStart)
{
$overlapStart = $minute;
}
// ... or track an existing one
else
{
$overlapUntil = $minute;
}
}
// If not and we were previously in a period of overlap, end it
elseif ($overlapStart)
{
$overlaps[] = [
'start' => $overlapStart,
'end' => $overlapUntil,
];
$overlapStart = null;
}
}
foreach ($overlaps as $overlap)
{
echo $overlap['start']->format('Y-m-d H:i:s'), ' -> ', $overlap['end']->format('Y-m-d H:i:s'), PHP_EOL;
}
There are some bugs with this implementation, see the comments. I'm unable to delete it as it's the accepted answer. Please use iainn or fusion3k's very good answers until I get around to fixing it.
There's actually no need to use any date/time handling to solve this
problem. You can exploit the fact that dates in this format are in alphabetical as well as chronological order.
I'm not sure this makes the solution any less complex. It's probably less
readable this way. But it's considerably faster than iterating over every minute so you might choose it if performance is a concern.
You also get to use
every
single
array
function
out there, which is nice.
Of course, because I haven't used any date/time functions, it might not work if Daylight Savings Time or users in different time zones need dealing with.
$availability = [
[
["2016-04-30 12:00", "2016-05-01 03:00"]
],
[
["2016-04-30 03:00", "2016-05-01 03:00"]
],
[
["2016-04-30 03:00", "2016-04-30 13:31"],
["2016-04-30 15:26", "2016-05-01 03:00"]
]
];
// Placeholder array to contain the periods when everyone is available.
$periods = [];
// Loop until one of the people has no periods left.
while (count($availability) &&
count(array_filter($availability)) == count($availability)) {
// Select every person's earliest date, then choose the latest of these
// dates.
$start = array_reduce($availability, function($carry, $ranges) {
$start = array_reduce($ranges, function($carry, $range) {
// This person's earliest start date.
return !$carry ? $range[0] : min($range[0], $carry);
});
// The latest of all the start dates.
return !$carry ? $start : max($start, $carry);
});
// Select each person's range which contains this date.
$matching_ranges = array_filter(array_map(function($ranges) use($start) {
return current(array_filter($ranges, function($range) use($start) {
// The range starts before and ends after the start date.
return $range[0] <= $start && $range[1] >= $start;
}));
}, $availability));
// Find the earliest of the ranges' end dates, and this completes our
// first period that everyone can attend.
$end = array_reduce($matching_ranges, function($carry, $range) {
return !$carry ? $range[1] : min($range[1], $carry);
});
// Add it to our list of periods.
$periods[] = [$start, $end];
// Remove any availability periods which finish before the end of this
// new period.
array_walk($availability, function(&$ranges) use ($end) {
$ranges = array_filter($ranges, function($range) use($end) {
return $range[1] > $end;
});
});
}
// Output the answer in the specified format.
foreach ($periods as $period) {
echo "$period[0] -> $period[1]\n";
}
/**
* Output:
*
* 2016-04-30 12:00 -> 2016-04-30 13:31
* 2016-04-30 15:26 -> 2016-05-01 03:00
*/
A different approach to your question is to use bitwise operators. The benefits of this solution are memory usage, speed and short code. The handicap is that — in your case — we can not use php integer, because we work with large numbers (1 day in minutes is 224*60), so we have to use GMP Extension, that is not available by default in most php distribution. However, if you use apt-get or any other packages manager, the installation is very simple.
To better understand my approach, I will use an array with a total period of 30 minutes to simplify binary representation:
$calendar =
[
'p1' => [
['start' => '2016-04-30 12:00', 'end' => '2016-04-30 12:28']
],
'p2' => [
['start' => '2016-04-30 12:10', 'end' => '2016-04-30 12:16'],
['start' => '2016-04-30 12:22', 'end' => '2016-05-01 12:30']
]
];
First of all, we find min and max dates of all array elements, then we init the free (time) variable with the difference in minutes between max and min. In above example (30 minutes), we obtain 230-20=1,073,741,823, that is a binary with 30 ‘1’ (or with 30 bits set):
111111111111111111111111111111
Now, for each person, we create the corresponding free-time variable with the same method. For the first person is easy (we have only one time interval): the difference between start and min is 0, the difference between end and min is 28, so we have 228-20=268435455, that is:
001111111111111111111111111111
At this point, we update global free time with a AND bitwise operation between global free time itself and person free time. The OR operator set bits if they are set in both compared values:
111111111111111111111111111111 global free time
001111111111111111111111111111 person free time
==============================
001111111111111111111111111111 new global free time
For the second person, we have two time intervals: we calculate each time interval with know method, then we compone global person free time using OR operator, that set bits if they are set in either first or second value:
000000000000001111110000000000 12:10 - 12:16
111111110000000000000000000000 12:22 - 12:30
==============================
111111110000001111110000000000 person total free time
Now we update global free time with the same method used for first person (AND operator):
001111111111111111111111111111 previous global free time
111111110000001111110000000000 person total free time
==============================
001111110000001111110000000000 new global free time
└────┘ └────┘
:28-:22 :16-:10
As you can see, at the end we have an integer with bits set only in minutes when everyone is available (you have to count starting from right). Now, you can convert back this integer to datetimes. Fortunately, GMP extension has a method to find 1/0 offset, so we can avoid to perform a for/foreach loop through all digits (that in real case are many more than 30).
Let's see the complete code to apply this concept to your array:
$calendar =
[
'p1' => [
['start' => '2016-04-30 12:00', 'end' => '2016-05-01 03:00']
],
'p2' => [
['start' => '2016-04-30 03:00', 'end' => '2016-05-01 03:00']
],
'p3' => [
['start' => '2016-04-30 03:00', 'end' => '2016-04-30 13:31'],
['start' => '2016-04-30 15:26', 'end' => '2016-05-01 03:00']
]
];
/* Get active TimeZone, then calculate min and max dates in minutes: */
$tz = new DateTimeZone( date_default_timezone_get() );
$flat = call_user_func_array( 'array_merge', $calendar );
$min = date_create( min( array_column( $flat, 'start' ) ) )->getTimestamp()/60;
$max = date_create( max( array_column( $flat, 'end' ) ) )->getTimestamp()/60;
/* Init global free time (initially all-free): */
$free = gmp_sub( gmp_pow( 2, $max-$min ), gmp_pow( 2, 0 ) );
/* Process free time(s) for each person: */
foreach( $calendar as $p )
{
$pf = gmp_init( 0 );
foreach( $p as $time )
{
$start = date_create( $time['start'] )->getTimestamp()/60;
$end = date_create( $time['end'] )->getTimestamp()/60;
$pf = gmp_or( $pf, gmp_sub( gmp_pow( 2, $end-$min ), gmp_pow( 2, $start-$min ) ) );
}
$free = gmp_and( $free, $pf );
}
$result = [];
$start = $end = 0;
/* Create resulting array: */
while( ($start = gmp_scan1( $free, $end )) >= 0 )
{
$end = gmp_scan0( $free, $start );
if( $end === False) $end = strlen( gmp_strval( $free, 2 ) )-1;
$result[] =
[
'start' => date_create( '#'.($start+$min)*60 )->setTimezone( $tz )->format( 'Y-m-d H:i:s' ),
'end' => date_create( '#'.($end+$min)*60 )->setTimezone( $tz )->format( 'Y-m-d H:i:s' )
];
}
print_r( $result );
Output:
Array
(
[0] => Array
(
[start] => 2016-04-30 12:00:00
[end] => 2016-04-30 13:31:00
)
[1] => Array
(
[start] => 2016-04-30 15:26:00
[end] => 2016-05-01 03:00:00
)
)
3v4l.org demo
Some additional notes:
At the start, we set $tz to current timezone: we will use it later, at the end, when we create final dates from timestamps. Dates created from timestamps are in UTC, so we have to set correct timezone.
To retrieve initial $min and $max values in minutes, firstly we flat original array, then we retrieve min and max date using array_column.
gmp_sub subtract second argument from first argument, gmp_pow raise number (arg 1) into power (arg 2).
In the final while loop, we use gmp_scan1 and gmp_scan0 to retrieve each ‘111....’ interval, then we create returning array elements using gmp_scan1 position for start key and gmp_scan0 position for end key.
I am trying to check if a post stored in a database is older then 1, 2, 3, and, finally, 4 days, respectively.
The table storing all of the posts has a date field. I have a query that retrieves the date and then I try to check if the date is older than 1, 2, 3 and 4 days, respectively and based on the result I want to move posts around the page.
I have the following:
foreach($this->getArticleData() as $i)
{
if(strtotime($i['date']) > strtotime('-1 day'))
{
$this->priority = '0.9';
}
elseif(strtotime($i['date']) > strtotime('-2 day'))
{
$this->priority = '0.8';
}
elseif(strtotime($i['date']) > strtotime('-3 day'))
{
$this->priority = '0.7';
}
else(strtotime($i['date']) > strtotime('-4 day'))
{
$this->priority = '0.6';
}
}
I do not think that code is working properly. In some instances the priorities are wrong. I am I using srttotime() function is is there another more reliable way to do this?
Forward slash (/) signifies American M/D/Y formatting, a dash (-) signifies European D-M-Y and a period (.) signifies ISO Y.M.D.
Your database uses the -'s, you're sure that's right? Anyway, a more sufficient way of your code would be:
foreach ( $this->getArticleData() as $i ) {
for ( $x = 1; $x < 10; $x++ ) {
if ( strtotime ( $i [ 'date' ] ) > strtotime ( '-'. $x ." day" ) ) {
$this->priority = '0.'. 10 - $i;
}
}
}
Flip your > around. You're checking if the date is greater/newer than the specified time. You'll also need to flip your if statement, because you're checking newest to oldest. A post that is 4 days old is also 1 day old, and will be caught by the first block.
I would prefer using a priority map and DateTime :
$list = array(
array( 'date' => '2014-12-14' ) ,
array( 'date' => '2014-12-15' ) ,
array( 'date' => '2014-12-14' ) ,
array( 'date' => '2014-12-11' ) ,
array( 'date' => '2014-12-14' ) ,
array( 'date' => '2014-12-13' ) ,
);
# maps days to priority
$priorities = array(
1 => 0.9,
2 => 0.8,
3 => 0.7,
4 => 0.6
);
$currentDate = new DateTime();
foreach( $list as $i ) {
$dateTime = new DateTime( $i['date'] );
$diff = $currentDate->diff( $dateTime );
$days = $diff->format( '%d' );
if( isset( $priorities[ $days ] ) ){
echo 'Date is: ' . $i['date'] . "| Difference is : " . $days . "| Priority is: " . $priorities[ $days ]. "<br/>";
}
}
Result:
Date is: 2014-12-14| Difference is : 1| Priority is: 0.9
Date is: 2014-12-14| Difference is : 1| Priority is: 0.9
Date is: 2014-12-11| Difference is : 4| Priority is: 0.6
Date is: 2014-12-14| Difference is : 1| Priority is: 0.9
Date is: 2014-12-13| Difference is : 2| Priority is: 0.8
i'm newbie in Cake and wodering how to insert multiple rows in a single saveall function,
i got this table,
CREATE TABLE IF NOT EXISTS `dates` (
`date` varchar(10) COLLATE utf8_unicode_ci NOT NULL
)
what i'm trying to do is let user select start date and end date using JQuery calander, once submit all the dates between this range will be saved into database, i already got the array of dates eg:
`array(
(int) 0 => '5/8/2013',
(int) 1 => '6/8/2013',
(int) 2 => '7/8/2013',
(int) 3 => '8/8/2013',
)
`
then my controller looks like this:
public function index(){
if ($this->request->is('post')) {
$this->Date->create();
$data = array();
$data['dates']=array();
$startDate = $this->request->data['Date']['from'];
$endDate = $this->request->data['Date']['to'];
$datesBlocked = $this->loopDates($this->request->data['Date']['from'],$this->request->data['Date']['to']);
$data['dates'][] = $this->request->data['Blockdate']['from'];
$data['dates'][] = $this->request->data['Blockdate']['to'];
/*foreach($datesBlocked as $data) {
$data['dates'][] = $data;
}*/
if($this->Date->saveAll($data)) {
$this->Session->setFlash(__('done'));
if ($this->Session->read('UserAuth.User.user_group_id') == 1) {
// $this->redirect("/manages");
}
}
}
public function loopDates($from,$to){
$blockdates = array();
$start = strtotime($from);
$end = strtotime($to);
debug($start);
$counter = 0;
for($t=$start;$t<=$end;$t+=86400) {
$d = getdate($t);
$blockdates[$counter++] = $d['mday'].'/'.$d['mon'].'/'.$d['year'];
}
debug($blockdates);
return $blockdates;
}
issue was i can't get foreach work, if i uncomment the foreach, i got error said Illegal string offset 'dates' , so i commented that and try to only add the start date and end date to the array to see if that works, then i got another error said.
`array(
'dates' => array(
(int) 0 => '08/05/2013',
(int) 1 => '09/05/2013'
)
)
`
Notice (8): Array to string conversion [CORE\Cake\Model\Datasource\DboSource.php, line 1005]Code
cuz i'm trying to insert 2 values into one field...i know it should be sth like
`array(
'dates' => array( (int) 0 => '08/05/2013',
)
'dates' => array((int) 1 => '09/05/2013'
))
`but can't figure out how to do it. Any help would be much appreciate!!!!
The structure you'll want your array to save multiple dates using saveAll() is this:
array(
'Date' => array(
0 => array(
'date' => '08/05/2013',
),
1 => array(
'date' => '09/05/2013',
)
),
)
I know that this is a little late, but to write multiple rows in a loop, you have to proceed the save with a create().
eg:
foreach($items as $lineItem){
$this->Invoice->create();
$this->Invoice->save(array(
'user_id'=>$property['User']['id'],
'invoice_id'=>$invId['Invoices']['id'],
'item_id'=>$lineItem['item_number'],
'quantity'=>$lineItem['quantity'],
'price'=>$lineItem['mc_gross']
);
}
Just thought it was worth mentioning, hopefully it will help someone.
I would like to make custom timestamps. I need to round the minute of the time to 00 or 30. I made already a PHP code for this:
if (date("i") >= '15' && date("i") < '45') {
$minute = "30";
}
else {
$minute = "00";
}
But, now, I want to make the timestamp with the time + date in it.
Does someone have a solution for this? I think I'll need to use strptime but I don't know how exactly..
You can use mktime to generate a timestamp rounded to the nearest 30 minutes:
echo date('Y-m-d H:i:s', mktime(date('H'), round(date('i') / 30) * 30, 0));
Example here:
http://codepad.org/3NCeWO21
The following code snippet:
<?php
date_default_timezone_set('America/New_York');
$format = '%d/%m/%Y %H:%M:%S';
$strf = strftime($format);
print_r(strptime($strf, $format));
?>
Produces this output:
Array
(
[tm_sec] => 49
[tm_min] => 48
[tm_hour] => 8
[tm_mday] => 14
[tm_mon] => 3
[tm_year] => 113
[tm_wday] => 0
[tm_yday] => 0
[unparsed] =>
)
I think you can take it from here.
I'm trying to do a web application with php and MySQL that can graph the estimate effort of a person based on the data stored in the data base
$luna_start = date('n', strtotime($row_task['data_i']));
$luna_end = date('n', strtotime($row_task['data_s']));
$luna_curenta=$luna_start;
$ultimazi_luna_curenta= cal_days_in_month(CAL_GREGORIAN, $luna_curenta,"2012");
while ($luna_curenta<=$luna_end){
//construieste vectorul
for ($i = 1; $i <= $ultimazi_luna_curenta; $i++) {
$data_curenta= date("Y-m-d", mktime(0, 0, 0, $luna_curenta, $i, 2012));
if (($data_curenta>=$row_task['data_i'])&&($data_curenta<=$row_task['data_s'])){
$efort_a['a'.$row_task['task_id']][$data_curenta]=$efort_mediu;
}
}
$luna_curenta++;
}
all that trouble for getting an array so that i can graph. the array $efort_a looks like this:
Array(
[a19] => Array(
[2012-09-20] => 2.84
[2012-09-21] => 2.84
.......
[2012-10-21] => 2.84
[2012-10-22] => 2.84
)
[a22] => Array
(
[2012-10-01] => 0.1
[2012-10-02] => 0.1
.....
[2012-11-05] => 0.1
[2012-11-06] => 0.1
[2012-11-07] => 0.1
......
[2012-11-25] => 0.1
......
[2012-11-30] => 0.1
)
[a16] => Array
(
[2012-10-08] => 4
[2012-10-09] => 4
[2012-10-10] => 4
[2012-10-11] => 4
......
[2012-10-18] => 4
[2012-10-19] => 4
)
)
further more I'm doing a little more array processing.
extract($efort_a);
$graf_data= array_add($a19,$a22,$a16);
$data = new GoogleChartData($graf_data);
$chart->addData($data);
the problem with all this code is that it isn't a dynamic one. the arrays a19, a22, a16 are named statically and in this particular case the user had only 3 tasks (task id 19, 22 and 16) ... but what if the user has multiple tasks ?.
so the questions is can this code be reorganized, maybe as a function i don't know how, to be a dynamic one ?
could it be possible to do a function that graph's all the users effort (one graph, multiple lines- one for each user)
ps: the function to do the adding of arrays based on keys is:
function array_add($a1, $a2) {
// adds the values at identical keys together
$aRes = $a1;
foreach (array_slice(func_get_args(), 1) as $aRay) {
foreach (array_intersect_key($aRay, $aRes) as $key => $val) $aRes[$key] += $val;
$aRes += $aRay;
}
return $aRes;
}
========================================= day 2 =======================
after some thinking i thought to give another try... so in order to name the keys of array effort_a dynamically i need to know where to stop with the incrementation so :
$sql_nrtask='SELECT *, COUNT(user) AS nr_task
FROM task
WHERE user="ASD"
ORDER BY data_i ASC
';
$query_nrtask= mysql_query($sql_nrtask) or die ('rrr');
while ($row_nrtask= mysql_fetch_array($query_nrtask)) {
$nr_task=$row_nrtask['nr_task']; // no we know how many sub-arrays we'll have
}
for ($j++;$j<=$nr_task;$j++){
while ($luna_curenta<=$luna_end){
//construieste vectorul
for ($i = 1; $i <= $ultimazi_luna_curenta; $i++) {
$data_curenta= date("Y-m-d", mktime(0, 0, 0, $luna_curenta, $i, 2012));
if (($data_curenta>=$row_task['data_i'])&&($data_curenta<=$row_task['data_s'])){
$efort_a['$a'.$j][$data_curenta]=$efort_mediu;
}
}
$luna_curenta++;
}
}
the problem is know that i don't know hot to merge the arrays within $efort_a with 2 conditions:
keeping indexes where they are the same and adding values
adding a new index where it is a new element