PHP确定多个(n)日期时间范围何时相互重叠

时间:2016-04-26 14:59:50

标签: php datetime

我有一段时间试图解决以下问题:

这是一个日历程序,其中给出了一组来自多个人的可用日期时间集,我需要弄清楚每个人在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

但我不知道那是什么语言,并且不得不在答案中翻译逻辑。

3 个答案:

答案 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,因此我们必须设置正确的时区。
  • 要在几分钟内检索初始$maxarray_column值,首先我们展平原始数组,然后使用while检索最小和最大日期。
  • 3v4l.org demo从第一个参数中减去第二个参数,gmp_sub将数字(arg 1)提升为幂(arg 2)。
  • 在最后的gmp_scan1循环中,我们使用gmp_powgmp_scan1来检索每个'111 ....'间隔,然后我们使用start创建返回的数组元素gmp_scan0键的位置和end键的buttons += "<button data-property='" + property + "'>" + property + "</button>"; 位置。