【问题标题】:PHP free time blocks from start and end dates开始和结束日期的 PHP 空闲时间块
【发布时间】:2017-04-07 07:54:49
【问题描述】:

我试图完全从 MySQL 查询中获取这些空闲时间块,但我无法构造一个与已有数据一起工作的查询。我选择简单地从查询中获取start 和end 日期并计算出PHP 中的空闲时间块。然而,这也被证明是困难的。

我有以下数组(只是一个例子,日期和时间会有所不同):

$meeting = array(array('start' => '2016-11-14 16:00:00',
                       'end'   => '2016-11-14 16:30:00'
                      ),
                 array('start' => '2016-11-14 16:45:00',
                       'end'   => '2016-11-14 20:00:00'
                      ),
                 array('start' => '2016-11-14 14:00:00',
                       'end'   => '2016-11-14 15:00:00'
                      ),
                 array('start' => '2016-11-14 13:00:00',
                       'end'   => '2016-11-14 14:00:00'
                      ),
                 array('start' => '2016-11-11 15:20:00',
                       'end'   => '2016-11-11 16:00:00'
                      ),
                 array('start' => '2016-11-11 14:00:00',
                       'end'   => '2016-11-11 15:00:00'
                      ),
                 array('start' => '2016-11-07 07:00:00',
                       'end'   => '2016-11-09 15:00:00'
                      )
               );

我想要结束的是一组日期,其中包含当天每个空闲时间段的开始和结束时间,仅在每天 09:00 到 17:00 之间,不包括周末。因此,例如,上面的会议时间数组应生成以下空闲时间数组:

$daily_free = array('2016-11-09' => array(array('free_start' => '15:00:00',
                                                'free_end'   => '17:00:00'
                                               )
                                         ),
                    '2016-11-10' => array(array('free_start' => '09:00:00',
                                                'free_end'   => '17:00:00'
                                               )
                                         ),
                    '2016-11-11' => array(array('free_start' => '09:00:00',
                                                'free_end'   => '14:00:00'
                                               ),
                                          array('free_start' => '15:00:00',
                                                'free_end'   => '15:20:00'
                                               ),
                                          array('free_start' => '16:00:00',
                                                'free_end'   => '17:00:00'
                                               )
                                         ),
                    '2016-11-14' => array(array('free_start' => '09:00:00',
                                                'free_end'   => '13:00:00'
                                               ),
                                          array('free_start' => '15:00:00',
                                                'free_end'   => '16:00:00'
                                               ),
                                          array('free_start' => '16:30:00',
                                                'free_end'   => '16:45:00'
                                               )
                                         )
                   );

您可以看到数组中只出现了有空闲时间的日子,11 月 7 日和 8 日没有空闲时间,所以它们根本不会出现。另外,11 月 12 日和 13 日没有出现在空闲时间数组中,这是因为它们是周末。但是,如果 12 日和 13 日不是周末,则它们应该以全天的空闲时间(09:00 到 17:00)出现在空闲时间数组中。

我能想到实现这一点的唯一方法之一是创建一个每天的数组,其中有一个从 09:00 到 17:00 的 5 分钟增量数组,其值为 NULL。然后循环遍历这个数组,检查每个日期和 5 分钟的增量是否在第一个数组的开始和结束时间之内,如果是,则将其标记为忙。然后我可以再次循环并获取每天空值的开始和结束时间?这似乎不是一个很好的解决方案,如果会议时间少于几分钟,也会失败。

我认为我现在看不到树木的树木,并且可能想多了。

还有其他解决方案吗?

编辑: SQL Fiddle 适合任何想通过 SQL 尝试的人

【问题讨论】:

  • 创建一个包含日期列表的表并在 SQL 中完成这项工作不是很简单吗?如果您在 SQLFiddle 上创建示例数据集,我可以展示如何做到这一点。
  • @AlexBlex SQL Fiddle 已添加到问题中,谢谢!
  • 顺便说一句,没有约会的日子会怎样?比如12和13?它们也应该是空闲时间,但您不要将它们包含在所需的输出中。
  • @phobia82 这是一个非常好的观点!如果他们不在周末,他们需要被包括在内。我将编辑我的问题以反映这一点。

标签: php mysql arrays datetime multidimensional-array


【解决方案1】:

与其使用您拥有的日期和日期,不如将值存储为DateTime 对象,然后使用这些对象将日期转换为 UNIX 时间戳。这些只是自 UNIX 纪元 (1970-01-01 00:00:00) 开始以来的秒整数值。

您现在可以通过简单的数学运算来找出单日范围内的间隔(将开始日期/时间和结束日期/一天中的时间转换为结束标记的时间戳)。

一旦将开始结束块标识为时间戳,您就可以将其反馈到 DateTime 对象中,并将其转换回标准日期表示。

识别空闲块的长度也很简单。由于这些值都是秒,因此从结束时间中减去开始时间将产生秒数差异。为确保大于 15 分钟,您可以将其与整数值 900 (60 * 15) 进行比较,如果大于则差距大于 15 分钟。

【讨论】:

  • 我不是 100% 确定将日期转换为时间戳有什么帮助?我仍然需要一些如何每天获得一次会议结束和下一次会议开始之间的范围?如果我在整数或实际日期对象中这样做有关系吗?
  • 转换为整数以简化日期,然后一个会议的结束变成一个数字,例如 1200,下一个会议从 4500 开始。你可以很容易地看到一个结束和开始之间有 3300 秒的差异的下一个。这也大于 15 分钟(900 秒),所以你现在知道你有一个从 1200 开始到 4500 结束的空闲块。显然这些整数对人类不友好,所以在显示结果时你需要转换回定期日期。 (我使用的值显然不是你在当前日期看到的:))
【解决方案2】:

我解决它的方法是尝试像从材料中雕刻一样思考问题。我会从充满空闲时间的一天开始 [09 - 17] 然后循环约会。对于每个约会,您要么拆分现有的空闲时间(然后按开始日期重新排序),要么更改范围。如果时间不重叠,那就容易多了。如果约会超出空闲时间范围,您可以忽略它。

{----}  {--------} Free Time Range
           [---]   Appointment in the middle - you need to split

{----}  {--------} Free Time Range
        [---]      Appointment at the beginning - change the start date

{----}  {--------} Free Time Range
             [---] Appointment at the end - change the end date

{----}  {--------} Free Time Range
        [--------] Appointment fills the whole spot - delete

【讨论】:

  • 我非常喜欢这个逻辑!也欢迎回到 StackOverflow!看起来你已经离开了一段时间:)
  • 考虑在您的范围之外开始或结束的约会,以及那些需要超过 1 天的约会,例如样本中的第 7 到 9 天
  • 多天,我会有其他东西可以将它分成多个约会并以相同的方式处理。
【解决方案3】:

按照@guided1 的逻辑,我开始运行一些测试,越走越远,我完成了整个算法的编写,就是这样。请注明@guided1

$meeting = array(array('start' => '2016-11-14 16:00:00',
                       'end'   => '2016-11-14 16:30:00'
                      ),
                 array('start' => '2016-11-14 16:45:00',
                       'end'   => '2016-11-14 20:00:00'
                      ),
                 array('start' => '2016-11-14 14:00:00',
                       'end'   => '2016-11-14 15:00:00'
                      ),
                 array('start' => '2016-11-14 13:00:00',
                       'end'   => '2016-11-14 14:00:00'
                      ),
                 array('start' => '2016-11-11 15:20:00',
                       'end'   => '2016-11-11 16:00:00'
                      ),
                 array('start' => '2016-11-11 14:00:00',
                       'end'   => '2016-11-11 15:00:00'
                      ),
                 array('start' => '2016-11-07 07:00:00',
                       'end'   => '2016-11-09 15:00:00'
                      )
               );
$min = "09:00:00";
$max = "17:00:00";
$start = "2030-01-01 00:00:00";
$end = "2000-01-01 00:00:00";
$free = [['start' => "2000-01-01 00:00:00", 'end' => "2030-01-01 00:00:00"]];
foreach($meeting as $m){
    foreach($free as $k=>$f){
        if($m['start']>$f['start'] && $m['start']<=$f['end']){
            $free[$k]['end'] = $m['start'];
            if($m['end']<$f['end'])
                $free[] = ['start'=>$m['end'], 'end'=>$f['end']];
        }
        elseif($m['end']<$f['end'] && $m['end']>$f['start']){
            $free[$k]['start'] = $m['end'];
        }
    }
    $start = min($start,$m['start']);
    $end = max($end,$m['end']);
}
$begin = new DateTime( $start );
$end = new DateTime( $end );

$interval = DateInterval::createFromDateString('1 day');
$period = new DatePeriod($begin, $interval, $end);

$daily_free = [];
foreach ( $period as $dt ){
    $m = [
        'start'=>date('Y-m-d ',strtotime($dt->format('Y-m-d').' -1 day')).$max,
        'end'=>$dt->format('Y-m-d ').$min
    ];
    foreach($free as $k=>$f){
        if($m['start']>$f['start'] && $m['start']<=$f['end']){
            $free[$k]['end'] = $m['start'];
            if($m['end']<$f['end'])
                $free[] = ['start'=>$m['end'], 'end'=>$f['end']];
        }
        elseif($m['end']<$f['end'] && $m['end']>$f['start']){
            $free[$k]['start'] = $m['end'];
        }
    }
}
foreach($free as $k=>$f){
    $s = explode(" ",$f['start']);
    $e = explode(" ",$f['end']);
    if($s[0] == $e[0]){
        $daily_free[$s[0]][] = ['start_free'=>$s[1], 'end_free'=>$e[1]];
    }
}
ksort($daily_free);
print_r($daily_free);

【讨论】:

  • 哇,经过测试,似乎可以完美运行!我会按要求将正确答案奖励给@g​​uided1,但如果允许,我会在 2 天内创建一个赏金并奖励你!感谢您加倍努力......
  • 哇。确实加倍努力!
【解决方案4】:

纯 MySQL 解决方案。不是很实用,更像是脑力锻炼:

SELECT r.* 
FROM (
    SELECT DISTINCT dd.`day`, 
        IF(dd.`ba` = 1, f.`free_before_start`, f.`free_after_start`) as `start`,  
        IF(dd.`ba` = 1, f.`free_before_end`, f.`free_after_end`) as `end`
    FROM (
        SELECT 
            a.`day`,
            IF(ISNULL(a.`id`) OR a.`before_day` < a.`day`, a.`day_start`, a.`before_end`) as `free_before_start`, 
            IF(ISNULL(a.`id`), a.`day_end`, a.`start`) as `free_before_end`,
            IF(ISNULL(a.`id`), a.`day_start`, a.`end`) as `free_after_start`, 
            IF(ISNULL(a.`id`) OR a.`next_day` > a.`day`, a.`day_end`, a.`next_start`) as `free_after_end`
        FROM (
            SELECT n.*, 
            b.`day` as `before_day`, b.`end` as `before_end`,
            t.`day` as `next_day`, t.`start` as `next_start`
            FROM (
                SELECT (@c1 := @c1 + 1) as `rowid`, s.*
                FROM (
                    SELECT d.`day`, d.`start` AS `day_start`, d.`end` AS `day_end`, 
                            m.`id`, GREATEST(d.`start`, m.`start`) AS `start`, LEAST(d.`end`, m.`end`) AS `end`     
                    FROM days d
                    LEFT JOIN `meetings` m ON 
                            m.start <= d.end 
                        AND m.end >= d.start
                    ORDER BY  2 ASC, 6 ASC
                ) s
                CROSS JOIN (SELECT @c1 := 0) AS dummy
            ) n
            LEFT JOIN 
            (
                SELECT (@c2 := @c2 + 1) as `rowid`, s.*
                FROM (
                    SELECT d.`day`, d.`start` AS `day_start`, d.`end` AS `day_end`, 
                            m.`id`, GREATEST(d.`start`, m.`start`) AS `start`, LEAST(d.`end`, m.`end`) AS `end`     
                    FROM days d
                    LEFT JOIN `meetings` m ON 
                            m.start <= d.end 
                        AND m.end >= d.start
                    ORDER BY  2 ASC, 6 ASC
                ) s
                CROSS JOIN (SELECT @c2 := 0) AS dummy
            ) t on t.`rowid` = n.`rowid` + 1
            LEFT JOIN 
            (
                SELECT (@c3 := @c3 + 1) as `rowid`, s.*
                FROM (
                    SELECT d.`day`, d.`start` AS `day_start`, d.`end` AS `day_end`, 
                            m.`id`, GREATEST(d.`start`, m.`start`) AS `start`, LEAST(d.`end`, m.`end`) AS `end`     
                    FROM days d
                    LEFT JOIN `meetings` m ON 
                            m.start <= d.end 
                        AND m.end >= d.start
                    ORDER BY  2 ASC, 6 ASC
                ) s
                CROSS JOIN (SELECT @c3 := 0) AS dummy
            ) b on b.`rowid` = n.`rowid` - 1
        ) a
    ) f 
    INNER JOIN (
        SELECT `day`, 1 as `ba` FROM `days` 
        UNION ALL
        SELECT `day`, 2 as `ba` FROM `days` 
        ORDER BY 1, 2
    ) dd on f.`day` = dd.`day`
    WHERE f.`free_before_start` <> f.`free_before_end` OR f.`free_after_start` <> f.`free_after_end`
) r
WHERE r.`start` <> r.`end`

需要working hours 表如下:

CREATE TABLE `days` (
  `day` date NOT NULL,
  `start` timestamp NOT NULL,
  `end` timestamp NOT NULL,
  PRIMARY KEY (`day`)
) ENGINE=InnoDB CHARSET=utf8;

INSERT INTO `days` 
    (`day`, `start`, `end`) 
VALUES 
    ('2016-11-06', '2016-11-06 07:00:00', '2016-11-06 17:00:00'),
    ('2016-11-07', '2016-11-07 07:00:00', '2016-11-07 17:00:00'),
    ('2016-11-08', '2016-11-08 07:00:00', '2016-11-08 17:00:00'),
    ('2016-11-09', '2016-11-09 07:00:00', '2016-11-09 17:00:00'),
    ('2016-11-10', '2016-11-10 07:00:00', '2016-11-10 17:00:00'), 
    etc, around 250 working days per year.

在整个代码中添加日期范围可以显着加快它的速度,但间隔很短,但它的可读性更差。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-03-21
    • 2022-11-16
    • 1970-01-01
    • 1970-01-01
    • 2021-09-06
    • 1970-01-01
    • 1970-01-01
    • 2019-10-04
    相关资源
    最近更新 更多