对于您的问题,另一种方法是使用位运算符。这种解决方案的好处是内存使用、速度和代码短小。缺点是,在您的情况下,我们不能使用 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']
]
];
首先,我们找到所有数组元素的最小和最大日期,然后用最大值和最小值之间的分钟差初始化自由(时间)变量。在上面的示例中(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
对于第二个人,我们有两个时间间隔:我们使用已知方法计算每个时间间隔,然后使用
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']
]
];
$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;
$free = gmp_sub( gmp_pow( 2, $max-$min ), gmp_pow( 2, 0 ) );
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;
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
位置为start
键和gmp_scan0
位置为end
键创建返回数组元素。