2010-05-07 8 views
19

Primera pregunta. Sé gentil.Cortar un intervalo de tiempo en las partes

Estoy trabajando en un software que hace un seguimiento del tiempo que los técnicos pasan trabajando en tareas. El software debe ser mejorado para reconocer diferentes multiplicadores de tasas facturables en función del día de la semana y la hora del día. (Por ejemplo, "Tiempo y medio después de las 5 PM de lunes a viernes")

La tecnología que utiliza el software solo necesita registrar la fecha, hora de inicio y hora de finalización (en horas y minutos). Se espera que el software rompa la entrada de tiempo en partes en los límites de cuando cambian los multiplicadores de velocidad. No se permite que una entrada de una sola vez abarque varios días.

Aquí hay una muestra parcial de la tabla de tasas. Las teclas de matriz de primer nivel son los días de la semana, obviamente. Las teclas de matriz de segundo nivel representan la hora del día en que se activa el nuevo multiplicador y se ejecuta hasta la siguiente entrada secuencial en la matriz. Los valores de matriz son el multiplicador para ese rango de tiempo.

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

En la llanura Inglés, esto representa una tasa de hora y media desde la medianoche hasta las 8 am, la tasa regular de 8 a 5 de la tarde, y la hora y media de nuevo desde 5 hasta las 11: 59 p. M. El momento en que ocurren estos descansos puede ser arbitrario para el segundo y puede haber un número arbitrario de ellos para cada día. (Este formato es completamente negociable, pero mi objetivo es hacerlo lo más legible posible)

Como ejemplo: una entrada de tiempo registrada el lunes de 15:00:00 (3 PM) a 21:00:00 (9 PM) consistiría en 2 horas facturadas a 1x y 4 horas facturadas a 1.5x. También es posible que una entrada de una sola vez abarque varios descansos. Con el ejemplo de la tabla de tarifas anterior, una entrada de tiempo de 6 a.m. a 9 p.m. tendría 3 subvaciamientos de 6-8 a.m. a.m. 1.5x, de 8 a.m. a 5 p.m. a 1 a.m., y de a 5.9 a.m. a a.m. a 1.5x. Por el contrario, también es posible que una entrada de hora solo sea de 08:15:00 a 08:30:00 y que esté comprendida por completo en el rango de un solo multiplicador.

Realmente podría usar algo de ayuda para programar algunos PHP (o al menos idear un algoritmo) que puede tomar un día de la semana, una hora de inicio y una hora de finalización y analizar en las subpartes requeridas. Sería ideal que el resultado fuera una matriz que consta de entradas múltiples para un triplete (inicio, finalización, multiplicador). Para el ejemplo anterior, la salida sería:

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

Yo simplemente no puedo hacerme a la lógica de la división de una sola (arranque, parada) en (potencialmente) múltiples subpartes.

+1

+1 para una excelente descripción y redacción. – JYelton

+2

+1 Bien, pedí una primera pregunta sobre SO. Dale algo de tiempo y algunas buenas respuestas vendrán. – webbiedave

+0

¿Cuál es la forma más adecuada de combinar algunas de estas respuestas en lo que realmente terminé usando? ¿Debo publicar una respuesta yo mismo? Edita la pregunta? ¿Alguno de ellos vale la pena o la pregunta es tan útil como es sin una solución "final"? No quiero (potencialmente) retirar los votos de las personas que ** realmente ** resolvieron mi problema. – beporter

Respuesta

0

Eineki descifró el algoritmo. La parte que falta en mis intentos fue tener el inicio y el tiempo de parada disponible en cada rango de multiplicador. Valoro la densidad de datos en mi tabla de tarifas original, así que usé las agallas de la rutina convert() de Eineki para tomar la tabla almacenada en config y agregar los tiempos de detención. Mi código ya se creó automáticamente (o rellenó) una tasa mínima tabla, lo que garantiza que el resto del código no se ahogue ni arroje advertencias/errores, así que lo incluí. También condensé bill() y map_shift() juntos ya que en mi opinión los dos no tienen ningún propósito útil el uno sin el otro.

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

?> 

Gracias a todos, especialmente a Eineki.

0

Yo sugeriría algo así como

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 

Podría ser más fácil de convertir todos los tiempos a nivel interno segundos para hacer los días/horas/minutos cálculos más fáciles de manejar.

0

Esto es básicamente una adaptación del algoritmo de @Loopo.

En primer lugar, sería bueno poder comparar veces usando > y <, por lo que primero convertimos todo momento (día de la semana + hora/minuto/segundo) a desplazamientos de tiempo 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; 
} 

Lo ideal es que esto ya se haya hecho (por ejemplo, almacena estas compensaciones convertidas en la base de datos en lugar de las cadenas originales xx:yy:zz D).

Ahora el divisor (con un ayudante, debido a la falta de cierres):

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

Y el uso de la clase:

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

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

var_dump($splitClockedTimes); 

Espero que esto ayude.

+0

Tuve la idea de convertir también a segundos, pero creo que hay una manera más limpia (tal vez no más eficiente). Posiblemente más propenso a errores, pero los campos provienen de la base de datos, por lo que siempre deben estar en un formato xx: yy: zz válido. Solo nos importan los tiempos relativos, por lo que podemos usar la Época Unix para darnos números pequeños con los que trabajar. función ConvertHoursToSeconds ($ horas) {return strtotime ('1 de enero de 1970'. $ Horas. 'UTC'); } – beporter

0

Aquí es mi método

Convertí todo para segundos para que sea mucho más fácil.

Aquí está la tabla de tasas indexada por segundos. Sólo hay 3 ranuras de tiempo para Lunes

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

Utiliza esta función para convertir hh: mm: ss al segundo

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

Ésta es la función que devuelve una tabla de tasas

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

Así es como lo usa

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

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

vuelve

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 
     ) 

) 

Básicamente bucles sobre el código de la tabla de tarifas y se encuentra el número de segundos desde la ranura de tiempo dado pertenecen a cada tasa.

+0

Esto es casi exactamente donde estaba cuando me di por vencido. ¡Gracias por ayudarme a darme cuenta de que estaba en el camino correcto! – beporter

2

Usaría un enfoque diferente, y cambiaré la representación de la tabla de tarifas en función de un par de consideraciones.

  • Los $ rateTable describen intervalos, ¿por qué no los codifica adecuadamente?
  • Lo que sucede en las fronteras (el martes y el lunes en mi ejemplo utilizan los dos enfoques diferentes de la definición de límite);
  • Los resultados que obtiene son de un tipo comparable pero usan una representación diferente.
  • 23: 59: 59 => parece un truco para mí. No puedo explicarlo ahora, pero tengo una campana sonando en la parte posterior de la cabeza diciéndome que tenga cuidado.

Por último, pero no menos importante, mi experiencia personal me permite decir que si no puede comprender un algoritmo, es probable que sus compañeros tengan las mismas dificultades (incluso si tiene éxito y resuelve problemas) y el código será una fuente primaria de error. Si encuentra una solución más simple y eficiente, será una ganancia de tiempo, dinero y dolores de cabeza. Tal vez será una ganancia incluso si la solución no es tan eficiente.

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

Por lo menos necesita una función para convertir el formato original al nuevo.

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

Y sí, se puede hacer la conversión sobre la marcha con

bill(convert($oldRateTable['Tuesday']),'07:15:00','17:00:00'); 

pero si te importa un poco de actuaciones ...

+0

Mi (ciertamente pequeña) preocupación con el cambio en la tabla de tarifas son los límites en dos puntos. Si las cosas cambian y comienzan a las 9:00 a.m. en lugar de las 8, alguien tiene que cambiar el rango de 00:00 a 08:00 * y * el rango de 08:00 a 17:00. No es un gran problema, pero ¿qué sucede si te olvidas de cambiar un extremo con un rango de 00:00 a 09:00 y un rango de 08:00 a 17:00? Estoy seguro de que los resultados van a ser inconsistentes. Sin embargo, no hay ninguna razón por la cual * el código * no puede agregar el límite de "detención" a cada rango de la tabla original. Aunque creo que has entendido la lógica. Voy a ejecutar algunas pruebas. – beporter

+0

En este caso, el resultado será seguramente distorsionado, pero señalaría que la entrada del usuario y la representación interna de datos impugnados son dos cosas diferentes. Para mí, esto es como argumentar que alguien puede ingresar las horas normales a las 12:00 p.m. o a las 25:00 p.m. Hay controles para hacer en la entrada. Puede preguntar al usuario (o al archivo de configuración) los límites en el formato en el que se siente menos propenso a errores. Mientras comprueba la corrección, puede convertir la entrada a un formato más manejable. – Eineki

Cuestiones relacionadas