Первый вопрос. Будьте нежны.
Я работаю над программным обеспечением, которое отслеживает потраченную работу времени технических персоналов над задачами. Программное обеспечение должно быть улучшено для распознавания различных оплачиваемых множителей уровня на основе дня недели и времени суток. (Например, "Оплата в полуторном размере после 17:00 в рабочие дни".)
Технология с помощью программного обеспечения только требуется, чтобы регистрировать дату, его время начала и его время остановки (в течение многих часов и минут). Программное обеспечение, как ожидают, разломает запись времени на части на границах того, когда множители уровня изменятся. Единственной записи времени не разрешают охватить в несколько дней.
Вот частичный образец списка тарифов. Ключи массива первого уровня являются днями недели, очевидно. Ключи массива второго уровня представляют время суток, когда новый множитель умирает и работает до следующей последовательной записи в массиве. Значения массива являются множителем для того диапазона времени.
[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:00, регулярный уровень с 20:00 до 17:00, и оплата в полуторном размере снова от 5 до 23:59. Время, когда эти повреждения происходят, может быть произвольным к второму и может быть произвольное число их в течение каждого дня. (Этот формат является совершенно договорным, но моя цель состоит в том, чтобы сделать его максимально легко человекочитаемым.)
Как пример: запись времени, зарегистрированная в понедельник с 15:00:00 (15:00) к 21:00:00 (21:00), состояла бы из 2 часов, тарифицированных в 1x и 4 часов, тарифицированных в 1.5x. Для единственной записи времени также возможно охватить несколько повреждений. Используя пример rateTable выше, запись времени с 6:00 до 21:00 имела бы 3 поддиапазона от 6-8 AM 1.5x, 8:00 - 17:00 1x, и PM 5-9 1.5x. В отличие от этого, также возможно, что запись времени может только быть с 8:15:00 до 8: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
)
)
Я просто не могу перенестись, моя голова вокруг логики разделения сингла (запустите, остановитесь) в (потенциально) несколько подразделений.
Eineki разгадал алгоритм. В моих попытках не хватало только начального и конечного времени в каждом диапазоне множителей. Мне важна плотность данных в моей оригинальной rateTable, поэтому я использовал внутренности Eineki's 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.
Я бы использовал другой подход и изменю представление rateTable, исходя из нескольких соображений.
И последнее, но не менее важное: мой личный опыт позволяет мне сказать, что если вы не можете понять алгоритм, скорее всего, ваши коллеги столкнутся с такими же трудностями (даже если вы добьетесь успеха и решите проблемы) и код будет основным источником ошибки. Если вы найдете более простое и эффективное решение, это будет выигрыш времени, денег и головной боли. Может быть, это будет выигрыш, даже если решение будет не столь эффективным.
$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');
, но если вас немного волнует производительность ...
Я бы предложил что-то вроде
получить общее время для выделения (рабочая остановка - рабочий запуск) найдите начальный слот (последний элемент, где time
Возможно, будет проще преобразовать все время в секунды внутренне, чтобы производить вычисления дней / часов / минут легче обрабатывать.
Вот мой метод
Я преобразовал все в секунды, чтобы упростить задачу.
Вот таблица тарифов, индексированная по секундам. В понедельник есть только 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
)
);
Эта функция используется для преобразования чч: мм: сс в секунды
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
)
)
В основном код просматривает таблицу тарифов и находит, сколько секунд из данного временного интервала принадлежит каждой скорости.
По сути, это адаптация алгоритма @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);
Надеюсь, это поможет.