array php time slice

array - php slice string



Cortando un rango de tiempo en partes (5)

Primera pregunta. Sé gentil.

Estoy trabajando en un software que rastrea el tiempo de los técnicos dedicados a las tareas. El software debe mejorarse para reconocer diferentes multiplicadores de tasas facturables según el día de la semana y la hora del día. (Por ejemplo, "Tiempo y medio después de las 5 PM en días laborables").

Solo se requiere que el técnico que usa el software registre la fecha, su hora de inicio y su 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 una sola entrada de tiempo para abarcar varios días.

Aquí hay una muestra parcial de la tabla de tasas. Las claves de la matriz de primer nivel son los días de la semana, obviamente. Las claves de la 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 la 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 términos sencillos, esto representa una tasa de tiempo y medio desde la medianoche hasta las 8 am, tasa regular de 8 a 5 pm y una vez y media de nuevo desde las 5 hasta las 11:59 pm. El momento en que ocurren estas interrupciones puede ser arbitrario para el segundo y puede haber un número arbitrario de ellas para cada día. (Este formato es completamente negociable, pero mi objetivo es hacer que sea tan fácil de leer como sea posible).

Como ejemplo: una entrada de tiempo registrada el lunes de 15:00:00 (3 PM) a 21:00:00 (9 PM) consistiría de 2 horas facturadas a 1x y 4 horas facturadas a 1.5x. También es posible que una sola entrada de tiempo abarque varios cortes. Usando el ejemplo de tabla de frecuencia anterior, una entrada de tiempo de 6 AM a 9 PM tendría 3 sub-rangos de 6-8 AM a 1.5x, 8 AM-5PM a 1x, y 5-9 PM a 1.5x. Por el contrario, también es posible que una entrada de tiempo solo sea de 08:15:00 a 08:30:00 y esté completamente incluida en el rango de un solo multiplicador.

Realmente podría usar un poco de ayuda para codificar algunos PHP (o al menos diseñar 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 la salida fuera una matriz que consta de varias entradas para un triplete (inicio, parada, 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 ) )

Simplemente no puedo envolver mi cabeza en torno a la lógica de dividir una sola (iniciar, detener) en (potencialmente) múltiples subpartes.


Aquí está mi método

Lo convertí todo en segundos para hacerlo mucho más fácil.

Aquí está la tabla de tasas indexada por segundos. Hay solo 3 ranuras de tiempo para el 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 a segundos

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

Esta 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 usas.

$start = ''2010-05-03T07:00:00''; $end = ''2010-05-03T21:00:00''; print_r( get_rates( $start, $end, $rate_table ) );

devoluciones

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, el código recorre la tabla de tasas y encuentra cuántos segundos del intervalo de tiempo dado pertenecen a cada tasa.


Eineki rompió el algoritmo. La parte que faltaba en mis intentos era tener el inicio y el tiempo de parada disponibles en cada rango de multiplicador. Valoro la densidad de los datos en mi tabla de frecuencia original, así que usé las agallas de la rutina convert () de Eineki para tomar la tabla almacenada en config y agregar los tiempos de parada. Mi código ya se creó automáticamente (o se completó) a una velocidad mínima Tabla, garantizando que el resto del código no se ahogue o 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://.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://.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.


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

Primero, sería bueno poder comparar los tiempos usando > y < , así que primero convertimos todos los tiempos (día de la semana + hora / minuto / segundo) en compensaciones 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; }

Idealmente, esto ya se habría hecho (por ejemplo, almacena estas compensaciones convertidas en la base de datos en lugar de las cadenas xx:yy:zz D originales de 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 usando 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.


Sugeriría algo 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 convertir todos sus tiempos en segundos internamente para que los cálculos de días / horas / minutos sean más fáciles de manejar.


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

  • La tabla $ rateTable describe los intervalos, ¿por qué no los codifica correctamente?
  • ¿Qué sucede en las fronteras (el martes y el lunes en mi ejemplo utilizan los dos enfoques diferentes para la definición de límites);
  • Los resultados que obtienes son de un tipo comparable pero usan una representación diferente.
  • 23: 59: 59 => me parece un truco. No puedo explicarlo ahora mismo, pero tengo una campana en la parte posterior de la cabeza que me dice que tenga cuidado.

Por último, pero no por ello menos importante, mi experiencia personal me permite decir que si no puede comprender mejor un algoritmo, es probable que sus compañeros de trabajo tengan las mismas dificultades (incluso si tiene éxito y resuelve los problemas) y el código Ser una fuente primaria de error. Si encuentra una solución más simple y eficiente, ganará tiempo, dinero y dolores de cabeza. Tal vez sea 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''));

Como mínimo, 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í, podrías hacer la conversión sobre la marcha con

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

Pero si te importan un poco las actuaciones ...