2010-05-07 99 views
19

第一個問題。要溫柔。將時間範圍切片成部分

我正在研究跟蹤技術人員在任務上花費的時間的軟件。該軟件需要增強,以便根據一週中的某一天和一天中的時間識別不同的計費率乘數。 (例如,「工作日下午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.5倍。單個時間條目也可能跨越多箇中斷。使用上面的示例rateTable,從上午6點到下午9點的時間條目將具有從6-8 AM @ 1.5x,8AM-5PM @ 1x和5-9PM @ 1.5x的3個子範圍。相比之下,時間條目也可能只是從08:15:00到08:30:00,並且完全包含在單個乘數的範圍內。

我真的可以使用一些幫助編碼一些PHP(或至少設計一個算法),可以採取一週的一天,開始時間和停止時間,並解析到所需的子部分。將輸出作爲包含多個入口(起始,停止,乘數)三元組的數組是理想的。對於上面的例子中,輸出將是:

[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 
      ) 
    ) 

我只是簡單不能完成我的頭圍繞分割的單個的邏輯(開始,停止)代入(潛在地)多個子部分。

+1

+1爲優秀的描述和措辭。 – JYelton 2010-05-07 22:35:01

+2

+1問了關於SO的第一個問題。給它一些時間,一些好的答案會來。 – webbiedave 2010-05-07 22:53:32

+0

什麼是合適的方式將這些答案中的一部分合併到我最終實際使用的內容中?我應該自己發表一個答案嗎?編輯問題?這兩者中的哪一個甚至是值得的,還是這個問題與沒有「最終」解決方案一樣有用?我不想(可能)從**實際上解決了我的問題的人那裏接受了投票。 – beporter 2010-05-10 14:12:35

回答

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。

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 

這可能是更容易的任何時候轉換成秒內使天/小時/分鐘計算,就好辦了。

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); 

希望這有助於。

+0

我的想法也轉換爲秒,但我認爲有更清潔(也許不是更有效)的方式。可能更容易出錯,但字段來自數據庫,所以它們應始終處於有效的xx:yy:zz格式。我們只關心相對的時間,所以我們可以使用Unix Epoch給我們一些小數字來處理。 function ConvertHoursToSeconds($ hours){return strtotime('January 1 1970'。$ hours。'UTC'); } – beporter 2010-05-09 21:20:59

0

這裏是我的方法

我把所有東西都轉換成了幾秒鐘,讓它變得更容易。

這是按秒索引的費率表。那裏有僅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 
     ) 

) 

基本上,代碼在速率表上循環,並查找來自給定時隙的秒數屬於每個速率。

+0

這幾乎就是我放棄時的地方。感謝您幫助我認識到我處在正確的軌道上! – beporter 2010-05-11 20:07:09

2

我會使用不同的方法,並且我會根據幾個注意事項來改變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'); 

,但如果你小心一點的表演...

+0

我對費率變化的擔憂(確實很少)是兩個地方的界限。如果情況發生變化,他們會在上午9點而不是8點開始正常工作,則必須更改00:00至08:00範圍*和* 08:00至17:00範圍內的時間。沒有什麼大不了的,但是如果你忘了把00:00到09:00範圍以及08:00到17:00範圍內的一端改掉,會發生什麼?我相信結果會不一致。但是,沒有理由爲什麼*代碼*不能將「停止」邊界添加到原始表的每個範圍。儘管我認爲你確定了邏輯。要運行一些測試。 – beporter 2010-05-09 21:17:54

+0

在這種情況下,我的結果肯定會出現亂碼,但我會指出用戶輸入和輸入數據的內部表示是兩個不同的東西。對我而言,這就像爭辯說有人可以在12點或25點結束正常的工作時間。對輸入進行檢查。你可以自由地以你不太容易出錯的格式向用戶(或配置文件)詢問邊界。在您檢查正確性時,您可以將輸入轉換爲更易於管理的格式。 – Eineki 2010-05-10 01:31:50