Разделение временного диапазона на части


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

Я работаю над программным обеспечением, которое отслеживает время, затрачиваемое техническими специалистами на выполнение задач. Программное обеспечение необходимо усовершенствовать, чтобы распознавать различные оплачиваемые множители ставок в зависимости от дня недели и времени суток. (Например, "Полтора часа после 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 часов, оплаченных за 1 и 4 часа выставляется счет в 1,5 раза. Также возможно, чтобы одна временная запись охватывала несколько перерывов. Используя приведенный выше пример таблицы ставок, запись времени с 6 утра до 9 вечера будет иметь 3 поддиапазона: 6-8 утра при 1,5 раза, 8 УТРА-5 вечера при 1 раз и 5-9 вечера при 1,5 раза. Напротив, также возможно, что запись времени может быть только с 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
            )
    )

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

Author: beporter, 2010-05-08

5 answers

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

  • Таблица ставок $описывает интервалы, почему вы не кодируете их должным образом?
  • Что происходит на границах (во вторник и понедельник в моем примере используются два разных подхода к определению границ);
  • Результаты, которые вы получаете, имеют сопоставимый тип, но используют другое представление.
  • 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
Author: Eineki, 2010-05-08 01:58:42

Вот мой метод

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

Вот таблица ставок, проиндексированная по секундам. На понедельник есть только 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
        )

)

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

 1
Author: Galen, 2010-05-08 01:31:59

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

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
Author: Loopo, 2010-05-07 23:43:08

В основном это адаптация алгоритма @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
Author: strager, 2010-05-08 00:47:57

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

?>

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

 0
Author: beporter, 2010-05-11 20:16:52