将时间范围切成部分

时间:2010-05-07 22:24:54

标签: php time slice

第一个问题。要温柔。

我正在研究跟踪技术人员完成任务所花费时间的软件。需要增强软件以基于星期几和一天中的时间识别不同的可计费率乘数。 (例如,“工作日下午5点以后的时间半”。)

使用该软件的技术人员只需记录日期,开始时间和停止时间(以小时和分钟为单位)。该软件有望在速率乘数改变时的边界处打破时间。一次性条目不允许跨越多天。

以下是费率表的部分样本。显然,第一级数组键是一周中的几天。第二级数组键表示新乘数启动时的时间,并运行到数组中的下一个连续条目。数组值是该时间范围的乘数。

[rateTable] => Array
    (
        [Monday] => Array
            (
                [00:00:00] => 1.5
                [08:00:00] => 1
                [17:00:00] => 1.5
                [23:59:59] => 1
            )

        [Tuesday] => Array
            (
                [00:00:00] => 1.5
                [08:00:00] => 1
                [17:00:00] => 1.5
                [23:59:59] => 1
            )
        ...
    )

简单来说,这表示从午夜到早上8点的时间,从下午8点到下午5点的正常率,以及从5点到晚上11:59再次的时间。这些中断发生的时间可以是第二次的任意时间,并且每天可以有任意数量的它们。 (这种格式完全可以协商,但我的目标是让它尽可能易于阅读。)

例如:星期一从15:00:00(下午3点)到21:00:00(晚上9点)记录的时间条目包括2小时计费1x和4小时计费1.5x。单个时间条目也可以跨越多个中断。使用上面的示例rateTable,从上午6点到下午9点的时间条目将具有3个子范围,从6-8 AM @ 1.5x,8 AM-5PM @ 1x,以及5-9 PM @ 1.5x。相比之下,时间输入也可能只是从08:15:00到08:30:00,并且完全包含在单个乘数的范围内。

我真的可以使用一些帮助编写一些PHP(或至少设计一个算法),这可能需要一周中的一天,一个开始时间和一个停止时间,并解析为所需的子部分。将输出作为一个由(start,stop,multiplier)三元组的多个条目组成的数组是理想的。对于上面的示例,输出将是:

[output] => Array
    (
        [0] => Array
            (
                [start] => 15:00:00
                [stop] => 17:00:00
                [multiplier] => 1
            )

        [1] => Array
            (
                [start] => 17:00:00
                [stop] => 21:00:00
                [multiplier] => 1.5
            )
    )

我只是简单地无法理解将单个(开始,停止)分成(可能)多个子部分的逻辑。

5 个答案:

答案 0 :(得分:3)

我会使用不同的方法,我会根据几个注意事项更改rateTable表示。

  • $ rateTable描述间隔,为什么不对它们进行正确编码?
  • 边界上发生的事情(我的例子中的星期二和星期一使用两种不同的边界定义方法);
  • 您获得的结果具有可比性,但使用不同的表示形式。
  • 23:59:59 =>对我来说似乎是个黑客。我现在无法解释,但是我的头后面响了一个铃,告诉我要注意它。

最后但并非最不重要的一点是,我的个人经历让我说,如果你无法将头脑包裹在一个算法上,你的同事可能会遇到同样的困难(即使你成功并解决了问题),代码将成为bug的主要来源。如果你找到一个更简单有效的解决方案,那将是一个时间,金钱和头痛的收获。即使解决方案不那么有效,也许它会获益。

$rateTable = array(
    'Monday' => array (
        array('start'=>'00:00:00','stop'=>'07:59:59','multiplier'=>1.5),
        array('start'=>'08:00:00','stop'=>'16:59:59','multiplier'=>1),
        array('start'=>'17:00:00','stop'=>'23:59:59','multiplier'=>1.5)
    ),
    'Tuesday'=> array (
        array('start'=>'00:00:00','stop'=>'08:00:00','multiplier'=>1.5),
        array('start'=>'08:00:00','stop'=>'17:00:00','multiplier'=>1),
        array('start'=>'17:00:00','stop'=>'23:59:59','multiplier'=>1.5)
    )
);

function map_shift($shift, $startTime, $stopTime)
{
    if ($startTime >= $shift['stop'] or $stopTime <= $shift['start']) {
        return;
    }
    return array(
        'start'=> max($startTime, $shift['start']),
        'stop' => min($stopTime, $shift['stop']),
        'multiplier' => $shift['multiplier']
    );
}

function bill($day, $start, $stop)
{
    $report = array();
    foreach($day as $slice) {
        $result = map_shift($slice, $start, $stop);
        if ($result) {
           array_push($report,$result);
        }
    }
    return $report;
}



/* examples */
var_dump(bill($rateTable['Monday'],'08:05:00','18:05:00'));
var_dump(bill($rateTable['Monday'],'08:05:00','12:00:00'));
var_dump(bill($rateTable['Tuesday'],'07:15:00','19:30:00'));
var_dump(bill($rateTable['Tuesday'],'07:15:00','17:00:00'));

至少你需要一个函数将原始格式转换为新格式。

$oldMonday = array (
   '00:00:00'=>1.5,
   '08:00:00'=>1,
   '17:00:00'=>1.5,
   '23:59:59'=>1
);

function convert($array) 
{
    return array_slice(
        array_map(
           function($start,$stop, $multiplier) 
           {
               return compact('start', 'stop','multiplier');
           },
           array_keys($array),
           array_keys(array_slice($array,1)),
           $array),
        0,
        -1);
}

var_dump(convert($oldMonday));

是的,您可以使用

动态进行转换
bill(convert($oldRateTable['Tuesday']),'07:15:00','17:00:00');

但如果你关心一些表演......

答案 1 :(得分:1)

这是我的方法

我将所有内容都转换为秒,以便更轻松。

这是以秒为单位索引的费率表。星期一只有3个时段

// 0-28800 (12am-8am) = 1.5
// 28800-61200 (8am-5pm) = 1
// 61200-86399 (5pm-11:50pm) = 1.5

$rate_table = array(
    'monday' => array (
        '28800' => 1.5,
        '61200' => 1,
        '86399' => 1.5
    )
);

它使用此函数将hh:mm:ss转换为秒

function time2seconds( $time ){
    list($h,$m,$s) = explode(':', $time);
    return ((int)$h*3600)+((int)$m*60)+(int)$s;
}

这是返回费率表的函数

function get_rates( $start, $end, $rate_table ) {

    $day = strtolower( date( 'l', strtotime( $start ) ) );

    // these should probably be pulled out and the function
    // should accept integers and not time strings
    $start_time = time2seconds( end( explode( 'T', $start ) ) );
    $end_time = time2seconds( end( explode( 'T', $end ) ) );

    $current_time = $start_time;

    foreach( $rate_table[$day] as $seconds => $multiplier ) {

        // loop until we get to the first slot
        if ( $start_time < $seconds ) {
            //$rate[ $seconds ] = ( $seconds < $end_time ? $seconds : $end_time ) - $current_time;

            $rate[] = array (

                'start' => $current_time,
                'stop' => $seconds < $end_time ? $seconds : $end_time,
                'duration' => ( $seconds < $end_time ? $seconds : $end_time ) - $current_time,
                'multiplier' => $multiplier

            );

            $current_time=$seconds;
            // quit the loop if the next time block is after clock out time
            if ( $current_time > $end_time ) break;
        }

    }

    return $rate;
}

以下是您使用它的方式

$start = '2010-05-03T07:00:00';
$end = '2010-05-03T21:00:00';

print_r( get_rates( $start, $end, $rate_table ) );

返回

Array
(
    [0] => Array
        (
            [start] => 25200
            [stop] => 28800
            [duration] => 3600
            [multiplier] => 1.5
        )

    [1] => Array
        (
            [start] => 28800
            [stop] => 61200
            [duration] => 32400
            [multiplier] => 1
        )

    [2] => Array
        (
            [start] => 61200
            [stop] => 75600
            [duration] => 14400
            [multiplier] => 1.5
        )

)

基本上,代码在速率表上循环,并查找给定时隙属于每个速率的秒数。

答案 2 :(得分:0)

我会建议像

这样的东西
get total time to allocate (workstop - workstart) 

find the start slot (the last element where time < workstart)
and how much of start slot is billable, reduce time left to allocate

move to next slot

while you have time left to allocate

   if the end time is in the same slot
       get the portion of the time slot that is billable
   else
       the whole slot is billable
       reduce the time to allocate by the slot time 


   (build your output array) and move to the next slot

loop while

在内部将所有时间转换为秒可能更容易,以使日/小时/分钟计算更容易处理。

答案 3 :(得分:0)

这基本上是对@ Loopo算法的改编。

首先,能够使用><比较时间会很好,所以首先我们将所有时间(星期几+小时/分钟/秒)转换为UNIX时间偏移:

// Code is messy and probably depends on how you structure things internally.

function timeOffset($dayOfWeek, $time) {
    // TODO Use standard libraries for this.
    $daysOfWeek = array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday');

    $splitTime = explode(':', $time);
    $offset = (((int)array_search($dayOfWeek, $daysOfWeek) * 24 + (int)$time[0]) * 60 + (int)$time[1]) * 60 + (int)$time[2];

    return $offset;
}

$rateTable = array(
    'Monday' => array(
        '00:00:00' => 1.5,
        '08:00:00' => 1,
        '17:00:00' => 1.5,
    ),

    'Tuesday' => array(
        '00:00:00' => 1.5,
        '08:00:00' => 1,
        '17:00:00' => 1.5,
    )
);

$clockedTimes = array(
    array('Monday', '15:00:00', '21:00:00')
);

$rateTableConverted = array();

foreach($rateTable as $dayOfWeek => $times) {
    foreach($times as $time => $multiplier) {
        $offset = timeOffset($dayOfWeek, $time);
        $rateTableConverted[$offset] = $multiplier;
    }
}

ksort($rateTableConverted);

$clockedTimesConverted = array();

foreach($clockedTimes as $clock) {
    $convertedClock = array(
        'start' => timeOffset($clock[0], $clock[1]),
        'end'   => timeOffset($clock[0], $clock[2]),
    );

    $clockedTimesConverted[] = $convertedClock;
}

理想情况下,这已经完成(例如,您将这些已转换的偏移量存储在数据库中而不是原始的xx:yy:zz D字符串中)。

现在是拆分器(由于缺少闭包而带有帮助器):

class BetweenValues {
    public $start, $end;

    public function __construct($start, $end) {
        $this->start = $start;
        $this->end = $end;
    }

    public function isValueBetween($value) {
        return $this->start <= $value && $value <= $this->end;
    }
}

class TimeRangeSplitter {
    private $rateTable;

    public function __construct($rateTable) {
        $this->rateTable = $rateTable;
    }

    private function getIntersectingTimes($times, $start, $end) {
        ksort($times);

        $betweenCalculator = new BetweenValues($start, $end);

        $intersecting = array_filter($times, array($betweenCalculator, 'isValueBetween'));

        /* If possible, get the time before this one so we can use its multiplier later. */
        if(key($intersecting) > 0 && current($intersecting) != $start) {
            array_unshift($intersecting, $times[key($intersecting) - 1]);
        }

        return array_values($intersecting);
    }

    public function getSplitTimes($start, $end) {
        $splits = array();

        $intersecting = $this->getIntersectingTimes(array_keys($this->rateTable), $start, $end);

        $curTime = $start;
        $curMultiplier = 0;

        foreach($intersecting as $sectionStartTime) {
            $splits[] = $this->getSplit($curTime, $sectionStartTime, $curMultiplier, $curTime);

            $curMultiplier = $this->rateTable[$sectionStartTime];
        }

        $splits[] = $this->getSplit($curTime, $end, $curMultiplier, $curTime);

        return array_filter($splits);
    }

    private function getSplit($time, $split, $multiplier, &$newTime) {
        $ret = NULL;

        if($time < $split) {
            $ret = array(
                'start' => $time,
                'end' => $split,
                'multiplier' => $multiplier,
            );

            $newTime = $split;
        }

        return $ret;
    }
}

使用课程:

$splitClockedTimes = array();
$splitter = new TimeRangeSplitter($rateTableConverted);

foreach($clockedTimesConverted as $clocked) {
    $splitClockedTimes[] = $splitter->getSplitTimes($clocked['start'], $clocked['end']);
}

var_dump($splitClockedTimes);

希望这有帮助。

答案 4 :(得分:0)

Eineki破解了算法。我尝试中缺少的部分是在每个乘数范围内开始可用的停止时间。我重视原始rateTable中的数据密度,所以我使用了Eineki的convert()例程的内容来获取存储在config中的表并添加停止时间。我的代码已经自动创建(或填充)了一个最小的速率表,保证其余代码不会窒息或抛出警告/错误,所以我把它包括在内。我还将bill()和map_shift()压缩在一起,因为在我看来,两者没有任何有用的目的。

<?php

//-----------------------------------------------------------------------
function CompactSliceData($start, $stop, $multiplier)
// Used by the VerifyRateTable() to change the format of the multiplier table.
{
    return compact('start', 'stop','multiplier');
}

//-----------------------------------------------------------------------
function VerifyAndConvertRateTable($configRateTable)
// The rate table must contain keyed elements for all 7 days of the week. 
// Each subarray must contain at LEAST a single entry for '00:00:00' => 
// 1 and '23:59:59' => 1. If the first entry does not start at midnight, 
// a new element will be added to the array to represent this. If given 
// an empty array, this function will auto-vivicate a "default" rate 
// table where all time is billed at 1.0x.
{
    $weekDays = array('Monday', 'Tuesday', 'Wednesday', 
            'Thursday', 'Friday', 'Saturday', 
            'Sunday',);  // Not very i18n friendly?     

    $newTable = array();
    foreach($weekDays as $day)
    {
        if( !array_key_exists($day, $configRateTable) 
            || !is_array($configRateTable[$day]) 
            || !array_key_exists('00:00:00', $configRateTable[$day]) )
        {
            $configRateTable[$day]['00:00:00'] = 1;
        }

        if( !array_key_exists($day, $configRateTable) 
            || !is_array($configRateTable[$day]) 
            || !array_key_exists('23:59:59', $configRateTable[$day]) )
        {
            $configRateTable[$day]['23:59:59'] = 1;
        }

        // Convert the provided table format to something we can work with internally.
        // Ref: http://stackoverflow.com/questions/2792048/slicing-a-time-range-into-parts
        $newTable[$day] = array_slice(
                array_map(
                   'CompactSliceData',
                   array_keys($configRateTable[$day]),
                   array_keys(array_slice($configRateTable[$day],1)),
                   $configRateTable[$day]),
                0,-1);
    }
    return $newTable;
}

//-----------------------------------------------------------------------
function SliceTimeEntry($dayTable, $start, $stop)
// Iterate through a day's table of rate slices and split the $start/$stop
// into parts along the boundaries.
// Ref: http://stackoverflow.com/questions/2792048/slicing-a-time-range-into-parts
{
    $report = array();
    foreach($dayTable as $slice) 
    {
        if ($start < $slice['stop'] && $stop > $slice['start'])
        {
           $report[] = array(
                    'start'=> max($start, $slice['start']),
                    'stop' => min($stop, $slice['stop']),
                    'multiplier' => $slice['multiplier']
                );
        }
    }
    return $report;
}


/* examples */
$rateTable = array(
    'Monday' => array('00:00:00' => 1.5, '08:00:00' => 1, '17:00:00' => 1.5),
    'Tuesday' => array('00:00:00' => 1.5, '08:00:00' => 1, '17:00:00' => 1.5),
    'Wednesday' => array('00:00:00' => 1.5, '08:00:00' => 1, '17:00:00' => 1.5),
    'Thursday' => array('00:00:00' => 1.5, '08:00:00' => 1, '17:00:00' => 1.5),
    'Friday' => array('00:00:00' => 1.5, '08:00:00' => 1, '17:00:00' => 1.5),
    'Saturday' => array('00:00:00' => 1.5, '15:00:00' => 2),
    'Sunday' => array('00:00:00' => 1.5, '15:00:00' => 2),
);

$rateTable = VerifyAndConvertRateTable($rateTable);

print_r(SliceTimeEntry($rateTable['Monday'],'08:05:00','18:05:00'));
print_r(SliceTimeEntry($rateTable['Monday'],'08:05:00','12:00:00'));
print_r(SliceTimeEntry($rateTable['Tuesday'],'07:15:00','19:30:00'));
print_r(SliceTimeEntry($rateTable['Tuesday'],'07:15:00','17:00:00'));

?>

谢谢大家,特别是Eineki。