试图解决以下问题:
这是一个日历程序,给定来自多个人的一组可用日期时间集,我需要弄清楚每个人在 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
我确实找到了类似的问题和答案日期时间 - 确定多个 (n) 个日期时间范围在 R 中是否相互重叠
但我不知道那是什么语言,不得不无法翻译答案中的逻辑。
这很有趣。可能有一种比每分钟循环更优雅的方法,但我不知道 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;
}
此实现存在一些错误,请参阅注释。我无法删除它,因为它是公认的答案。请使用iainn或fusion3k非常好的答案,直到我开始修复它。
实际上没有必要使用任何日期/时间处理来解决这个问题问题。您可以利用此格式的日期按字母顺序和时间顺序排列的事实。
我不确定这是否会使解决方案变得不那么复杂。可能更少这样可读。但它比每分钟迭代要快得多,因此如果性能是一个问题,您可以选择它。
您还可以使用每单数组功能外面,这很好。
当然,因为我没有使用任何日期/时间函数,所以如果夏令时或不同时区的用户需要处理,它可能不起作用。
$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
*/
解决问题的不同方法是使用按位运算符。此解决方案的优点是内存使用、速度和短代码。障碍是 - 在您的情况下 - 我们不能使用 php 整数,因为我们处理大数字(1 天以分钟为单位是 224*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']
]
];
首先,我们找到所有数组元素的最小和最大日期,然后我们初始化自由(时间)变量,其中 max 和 min 之间的分钟差。在上面的例子(30 分钟)中,我们得到 2 30-2 0=1,073,741,823,这是一个具有30 个 '1' 的二进制文件(或设置了 30 位):
111111111111111111111111111111
现在,对于每个人,我们使用相同的方法创建相应的空闲时间变量。对于第一人称很容易(我们只有一个时间间隔):开始和min的差是0,结束和min的差是28,所以我们有228-20=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
如您所见,最后我们有一个整数,当每个人都有空时,位仅以分钟为单位设置(您必须从右开始计数)。现在,您可以将此整数转换回日期时间。幸运的是,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( 'Y-m-d H:i:s' ),
'end' => date_create( '@'.($end+$min)*60 )->setTimezone( $tz )->format( 'Y-m-d 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
值,首先我们平整原始数组,然后使用array_column
检索最小和最大日期。 -
gmp_sub
从第一个参数中减去第二个参数,gmp_pow
将数字 (arg 1) 提高到幂 (arg 2)。 - 在最后的
while
循环中,我们使用gmp_scan1
和gmp_scan0
来检索每个 '111...." 间隔,然后我们使用gmp_scan1
键的位置和end
键的位置来创建返回数组元素start
gmp_scan0
位置。