У меня есть время, пытаясь решить следующую проблему:
Это программа календаря, где задан набор доступных наборов дат-времени от нескольких людей, мне нужно выяснить, какие диапазоны дат-времени доступны всем в PHP
Доступные наборы:
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"
Я ищу функцию, которую я могу назвать, которая скажет мне, что число в датах охватывает все (p) людей одновременно.
В приведенном выше примере ответ должен быть:
2016-04-30 12:00 -> 2016-04-30 13:31 2016-04-30 15:26 -> 2016-05-01 03:00
Я нашел этот похожий вопрос и ответ Datetime. Определите, перекрываются ли множественные (n) диапазоны datetime друг с другом в R
Но я понятия не имею, какой язык есть, и не должен переводить логику в ответ.
На самом деле нет необходимости использовать обработку даты и времени для решения этой проблемы. Вы можете использовать тот факт, что даты в этом формате находятся в алфавитном порядке, а также в хронологическом порядке.
Я не уверен, что это делает решение менее сложным. Это, вероятно, менее читаемо. Но это значительно быстрее, чем повторение в течение каждой минуты, поэтому вы можете выбрать его, если производительность является проблемой.
Вы также можете использовать каждую функцию массива , что приятно.
Конечно, поскольку я не использовал функции даты и времени, это может не сработать, если нужно использовать Daylight Savings Time или пользователей в разных часовых поясах.
$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 = []; while (true) { // 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)); // If anybody doesn't have a range containing the date, we're finished // and can exit the loop. if (count($matching_ranges) < count($availability)) { break; } // 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 */
Ну, это было весело. Вероятно, это более элегантный способ сделать это, чем перебирать каждую минуту, но я не знаю, является ли PHP для него языком. Обратите внимание, что для этого в настоящее время требуется управлять временем начала и окончания для поиска отдельно, хотя было бы довольно тривиально рассчитать их на основе доступных сдвигов.
<?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('Ymd H:i:s'), ' -> ', $overlap['end']->format('Ymd H:i:s'), PHP_EOL; }
Другой подход к вашему вопросу – использовать побитовые операторы. Преимущества этого решения – использование памяти, скорость и короткий код. Недостатком является то, что в вашем случае мы не можем использовать целое число php, потому что мы работаем с большими числами (1 день в минутах – 2 24 * 60 ), поэтому нам нужно использовать расширение GMP , которое по умолчанию недоступно в большинстве php. Однако, если вы используете apt-get
или любой другой менеджер пакетов, установка очень проста .
Чтобы лучше понять мой подход, я буду использовать массив с общим периодом 30 минут, чтобы упростить двоичное представление:
$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'] ] ];
Прежде всего, мы находим минимальные и максимальные даты всех элементов массива, затем мы запускаем свободную (временную) переменную с разницей в минутах между макс и мин. В приведенном выше примере (30 минут) получаем 2 30 -2 0 = 1 073 741 823, то есть двоичный код с 30 '1' (или с набором из 30 бит):
111111111111111111111111111111
Теперь для каждого человека мы создаем соответствующую переменную времени с тем же методом. Для первого человека легко (у нас есть только один временной интервал): разница между начальным и минимальным значениями равна 0, разница между концом и минусом равна 28, поэтому мы имеем 2 28 -2 0 = 268435455, то есть:
001111111111111111111111111111
На этом этапе мы обновляем глобальное свободное время с помощью побитовой операции AND
между глобальным свободным временем и свободным временем человека. Оператор OR
устанавливает биты, если они установлены в обоих сравниваемых значениях:
111111111111111111111111111111 global free time 001111111111111111111111111111 person free time ============================== 001111111111111111111111111111 new global free time
Для второго человека у нас есть два временных интервала: мы вычисляем каждый временной интервал с помощью метода know, затем мы составляем глобальное свободное время человека с помощью оператора OR
, который устанавливает биты, если они установлены в первом или втором значении:
000000000000001111110000000000 12:10 - 12:16 111111110000000000000000000000 12:22 - 12:30 ============================== 111111110000001111110000000000 person total free time
Теперь мы обновляем глобальное свободное время с помощью того же метода, который используется для оператора первого лица ( AND
):
001111111111111111111111111111 previous global free time 111111110000001111110000000000 person total free time ============================== 001111110000001111110000000000 new global free time └────┘ └────┘ :28-:22 :16-:10
Как вы можете видеть, в конце у нас есть целое число с битами, установленным только в минутах, когда все доступны (вам нужно начинать отсчет справа). Теперь вы можете преобразовать это целое число в datetimes. К счастью, расширение GMP
имеет способ найти смещение 1/0, поэтому мы можем избежать выполнения цикла for / foreach через все цифры (что в реальном случае намного больше 30).
Давайте рассмотрим полный код для применения этой концепции к вашему массиву:
$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( 'Ymd H:i:s' ), 'end' => date_create( '@'.($end+$min)*60 )->setTimezone( $tz )->format( 'Ymd H:i:s' ) ]; } print_r( $result );
Вывод:
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
Некоторые дополнительные примечания:
$tz
в текущий часовой пояс: мы будем использовать его позже, в конце, когда мы создадим окончательные даты из временных меток. Даты, созданные из временных меток, находятся в формате UTC, поэтому мы должны установить правильный часовой пояс. $min
и $max
за считанные минуты, сначала мы имеем плоский исходный массив, затем получаем min и max date с помощью array_column
. gmp_sub
вычитает второй аргумент из первого аргумента, gmp_pow
число (arg 1) к мощности (arg 2). gmp_scan1
и gmp_scan0
для извлечения каждого интервала «111 ….», затем создаем возвращаемые элементы массива с использованием позиции gmp_scan1
для ключа start
и позиции gmp_scan0
для end
ключа.