我有一段时间试图解决以下问题:
这是一个日历程序,其中给出了一组来自多个人的可用日期时间集,我需要弄清楚每个人在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 -Determine whether multiple(n) datetime ranges overlap each other in R
但我不知道那是什么语言,并且不得不在答案中翻译逻辑。
答案 0 :(得分:4)
这很有趣。这可能是一种更优雅的方式,而不是循环每一分钟,但我不知道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('Y-m-d H:i:s'), ' -> ', $overlap['end']->format('Y-m-d H:i:s'), PHP_EOL;
}
答案 1 :(得分:1)
实际上没有必要使用任何日期/时间处理来解决这个问题 问题。您可以利用这种格式的日期按字母顺序和时间顺序排列的事实。
我不确定这会使解决方案变得更简单。它可能更少 这样可读。但它比每分钟迭代要快得多,因此如果性能受到关注,你可以选择它。
你也可以使用 every single array function out there,这很好。
当然,因为我没有使用任何日期/时间功能,如果夏令时或不同时区的用户需要处理,它可能无效。
$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
*/
答案 2 :(得分:1)
您的问题的另一种方法是使用按位运算符。此解决方案的好处是内存使用,速度和短代码。差点是 - 在你的情况下 - 我们不能使用php整数,因为我们使用大数字(1分钟,分钟是2 24 * 60 ),所以我们必须使用{{3}在大多数php发行版中默认不可用。但是,如果您使用GMP Extension或任何其他软件包管理器,则安装为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位):< / p>
111111111111111111111111111111
现在,对于每个人,我们使用相同的方法创建相应的自由时间变量。对于第一个人很容易(我们只有一个时间间隔):start和min之间的差值为0,end和min之间的差值为28,所以我们有2 28 -2 0 = 268435455,即:
001111111111111111111111111111
此时,我们使用全局空闲时间本身和人员空闲时间之间的AND
按位运算来更新全局空闲时间。如果OR
运算符在两个比较值中设置,则111111111111111111111111111111 global free time
001111111111111111111111111111 person free time
==============================
001111111111111111111111111111 new global free time
运算符设置位:
OR
对于第二个人,我们有两个时间间隔:我们使用know方法计算每个时间间隔,然后使用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
运营商)相同的方法更新全球空闲时间:
GMP
正如你所看到的,最后我们有一个整数,当每个人都可用时,你的位数只能在几分钟内设置(你必须从右边开始计算)。现在,您可以将此整数转换回日期时间。幸运的是,$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 );
扩展有一个方法可以找到1/0偏移量,因此我们可以避免在所有数字中执行for / foreach循环(在实际情况下,这个数字超过30)。
让我们看看将此概念应用于您的数组的完整代码:
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
)
)
输出:
$tz
的 very simple 强>
一些补充说明:
$min
设置为当前时区:我们将在稍后使用它,最后,当我们从时间戳创建最终日期时。从时间戳创建的日期是UTC,因此我们必须设置正确的时区。$max
和array_column
值,首先我们展平原始数组,然后使用while
检索最小和最大日期。gmp_sub
将数字(arg 1)提升为幂(arg 2)。gmp_scan1
循环中,我们使用gmp_pow
和gmp_scan1
来检索每个'111 ....'间隔,然后我们使用start
创建返回的数组元素gmp_scan0
键的位置和end
键的buttons += "<button data-property='" + property + "'>" + property + "</button>";
位置。