Подтвердить что ты не робот

Нарезка временного диапазона на части

Первый вопрос. Будьте осторожны.

Я работаю над программным обеспечением, которое отслеживает время техников, потраченное на выполнение задач. Программное обеспечение должно быть расширено, чтобы распознавать различные множители коэффициентов оплаты в зависимости от дня недели и времени суток. (Например, "Время и полтора после пяти вечера в будние дни".)

Технология, использующая программное обеспечение, требуется только для регистрации даты, времени начала и времени остановки (в часах и минутах). Предполагается, что программное обеспечение будет прерывать ввод времени в части на границах, когда изменяются коэффициенты умножения. Единовременная запись не разрешается охватывать несколько дней.

Вот частичная выборка таблицы тарифов. Очевидно, ключи массива первого уровня - это дни недели. Ключи массива второго уровня представляют собой время дня, когда новый множитель запускается и запускается до следующей последовательной записи в массиве. Значения массива являются множителем для этого временного диапазона.

[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 до 23:59. Время, в течение которого происходят эти разрывы, может быть произвольным ко второму, и для каждого дня может быть произвольное число из них. (Этот формат полностью подлежит обсуждению, но моя цель - сделать его максимально понятным для человека.)

В качестве примера: временная запись, зарегистрированная в понедельник с 15:00:00 (с 15:00 до 21:00:00 (9 часов)), будет состоять из 2 часов, выставленных на счет 1x и 4 часа с оплатой в 1,5 раза. Также возможно, чтобы однократная запись включала несколько разрывов. Используя пример rateTable выше, запись времени с 6:00 до 21:00 будет иметь 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
            )
    )

Я просто не могу обернуть голову вокруг логики разделения одного (старт, стоп) на (потенциально) несколько подчастей.

4b9b3361

Ответ 1

Эйнеки взломал алгоритм. Часть, отсутствующая в моих попытках, имела начало и время остановки, доступное в каждом диапазоне множителей. Я оцениваю плотность данных в своей исходной 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 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'));

?>

Спасибо всем, особенно Эйнеки.

Ответ 2

Я бы использовал другой подход, и я изменю представление RateTable на основе нескольких соображений.

  • $rateTable описывает интервалы, почему бы вам не закодировать их правильно?
  • Что происходит на границах (во вторник и в понедельник в моем примере используются два разных подхода к определению границ);
  • Полученные вами результаты имеют сопоставимый тип, но используют другое представление.
  • 23: 59: 59 = > кажется мне взломан. Сейчас я не могу объяснить, но у меня звон колокольчика в затылке, который говорит мне следить за этим.

И последнее, но не менее важное: мой личный опыт позволяет мне сказать, что если вы не можете обернуть голову алгоритмом, вероятно, ваши коллеги будут иметь те же трудности (даже если вам удастся и решить проблемы) и код будет основным источником ошибок. Если вы найдете более простое и эффективное решение, это будет прибыль времени, денег и головных болей. Возможно, это будет выигрыш, даже если решение не так эффективно.

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

но если вы немного поработаете...

Ответ 3

Я бы предложил что-то вроде

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

Может быть проще преобразовать все ваше время в несколько секунд, чтобы упростить обработку вычислений дней/часов/минут.

Ответ 4

Это в основном адаптация алгоритма @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);

Надеюсь, что это поможет.

Ответ 5

Здесь мой метод

Я преобразовал все в несколько секунд, чтобы сделать его намного проще.

Здесь индексная таблица индексируется по секундам. Theres только 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 to seconds

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
        )

)

В основном код перебирается по таблице скоростей и находит, сколько секунд от данного временного интервала принадлежит каждой скорости.