Создавать диапазон из массива дат

96
31 марта 2022, 12:10

Имеется массив дат с подмассивами из времён. Например:

Array
(
[2020-02-07] => Array
    (
        [09:00] => 1500
        [10:30] => 1500
        [11:00] => 1500
        [11:30] => 1500
    )
[2020-02-08] => Array
    (
        [09:00] => 1500
        [09:30] => 1500
        [11:00] => 1500
    )
)

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

Array
(
[0] => Array
    (
        [date] => 2020-02-07
        [range] => 9:00
    )
[1] => Array
    (
        [date] => 2020-02-07
        [range] => 10:30 - 11:30
    )
[2] => Array
    (
        [date] => 2020-02-08
        [range] => 09:00 - 09:30
    )
[3] => Array
    (
        [date] => 2020-02-08
        [range] => 11:00
    )
)

У меня пока мало идей и получается совсем что-то непотребное! Вот код:

    //Инициализируем массив на выходе
    $order = array();
    //Инициализируем счетчик для ключей в массиве на выходе
    $counter = 0;
    //$data - это тот самый массив в начале вопроса
    foreach($data as $date => $timesArr) {
        $order[$counter]['date'] = $date;
        $timesArrKeys = array_keys($timesArr);
        $start = current($timesArrKeys);
        if(count($timesArrKeys) > 1) {
            $range = array();
            while ($time = current($timesArrKeys)) {
                $next = next($timesArrKeys);
                if((strtotime($next) - strtotime($time)) == 1800) {
                    $range = $start . ' - ' . $next;
                    $order[$counter]['range'] = $range;
                }else{
                    $start = $next;
                    $counter++;
                    $order[$counter]['date'] = $date;
                    $order[$counter]['range'] = $start;
                }
                next($timesArrKeys);
            }   
        }else{
            $order[$counter]['date'] = $date;
            $order[$counter]['range'] = current($timesArrKeys);
        }
    }

По сути, этот код умеет формировать диапазоны, но он пропускает первые элементы, у которых отсутствует следующий элемент.

Answer 1

Чутка упростил входные значения чтоб не загромождать код. Суть такая: в цикле оперируем тремя курсорами - на текущее значение в итерации и на два прошлых - с которого начинался диапазон и ровно предыдущее значение в цикле. Теперь принимаем очень простое решение и либо бежим дальше, либо накапливаем результат.

function extractRangeList(array $in): array
{
    $out = [];
    $from = null;
    $prev = null;
    foreach ($in as $time) {
        // начало нового диапазона
        if (null === $from) {
            $from = $time;
            $prev = $time;
            continue;
        }
        $inc = strtotime($time) - strtotime($prev);
        // продолжение диапазона найденного ранее
        if ($inc === 1800) {
            $prev = $time;
            continue;
        }
        // конец диапазона
        $out[] = $from === $prev
            ? "{$from}"
            : "{$from} - {$prev}";
        $from = $time;
        $prev = $time;
    }
    // вышли не закрыв диапазон?
    if ($from !== null) {
        $out[] = $from === $prev
            ? "{$from}"
            : "{$from} - {$prev}";
    }
    return $out;
}
$in = [
    'A' => ['09:00' => 0, '10:30' => 0, '11:00' => 0, '11:30' => 0],
    'B' => ['09:00' => 0, '09:30' => 0, '11:00' => 0],
];
$expected = [
    ['date' => 'A', 'range' => '09:00'],
    ['date' => 'A', 'range' => '10:30 - 11:30'],
    ['date' => 'B', 'range' => '09:00 - 09:30'],
    ['date' => 'B', 'range' => '11:00'],
];
$out = [];
foreach ($in as $date => $prices) {
    $rangeList = extractRangeList(array_keys($prices));
    foreach ($rangeList as $range) {
        $out[] = ['date' => $date, 'range' => $range];
    }
}
assert($expected === $out);
Answer 2

В общем, решил эту задачу. На данный момент, пока очень криво и в несколько циклов. Но хотя бы понят алгоритм. Осталось только код сделать более изящным, но это я уже сам! Вот код:

$order = array(); //инициализируем массив для конечного результата
$counter = 0; //Это ключ для отделения позиций в $order
foreach($data as $date => $timesArr) {
    $timesArrKeys = array_keys($timesArr); //Со значениями работать проще чем с ключами
    //Если только 1 элемент в массиве то инкрементируем $counter на всякий случай и добавляем его в общий массив
    if(count($timesArrKeys) > 1) {
        $begin = 0; //Индикатор того начался ли диапазон
        $counter++;
        foreach($timesArrKeys as $k => $v) {
            //Объявляем сразу следующий элемент в итерации для проверки
            //Обращаю внимание, что для того, чтобы эта переменная не была статичной в каждой итерации, нужно руками смещать указатель через next()
            $next = current($timesArrKeys);
            if($begin) {
                $order[$counter]['date'] = $date;
                $order[$counter]['range'] .= $v . ',';
                if((strtotime($next) - strtotime($v)) == 1800) {
                    next($timesArrKeys);
                    continue;
                }else{
                    $begin = 0;
                    next($timesArrKeys);
                    continue;
                }
            }
            if((strtotime($next) - strtotime($v)) == 1800) {
                $counter++;
                $begin = 1;
                $order[$counter]['date'] = $date;
                $order[$counter]['range'] .= $v . ',';
            }else{
                $counter++;
                $begin = 0;
                $order[$counter]['date'] = $date;
                $order[$counter]['range'] = $v;
            }
            next($timesArrKeys);
        }
    }else{
        $counter++;
        $order[$counter]['date'] = $date;
        $order[$counter]['range'] = current($timesArrKeys);
    }
}
$order = array_values($order);
foreach($order as $k => &$v) {
    if(!empty($v['range']) && (strpos($v['range'], ',') || strpos($v['range'], ',') === 0)) {
        $v['range'] = rtrim($v['range'], ',');
        $array = explode(',', $v['range']);
        if(count($array) >= 2) {
            $v['range'] = array_shift($array) . ' - ' . array_pop($array);
        }
    }
}
return $order;

Т.е. смысл здесь следующий: В начале цикла с датами $timesArrKeys проверяем на предмет $begin т.е. не начался ли период в цикле когда текущий и следующий элемент с разницей в 30 минут. Изначально $begin - 0 поэтому при первой итерации этот участок пропускается и переходит сразу к следующему участку - if((strtotime($next) - strtotime($v)) == 1800) где мы сверяем текущий элемент и следующий в цикле и если тут true то выставляем $begin = 1, что значит, что при следующей итерации сработает первое условие в котором уже будет проводиться проверка на наличие следующего элемента, удовлетворяющего диапазону и если удовлетворяет, то мы просто смещаем указатель для нашего $next и пропускаем итерацию, а на следующей мы опять попадем под $begin и добавим через запятую элемент, т.к. в прошлой итерации мы выяснили что он подходит, но вот если уже следующий элемент не подходит то сработает else где мы выставим $begin = 0 и следующий элемент подпадет под корневой else в этом foreach. $counter получится сбитый, но он нам не принципиален и его можно обнулить через array_values как я и сделал для второго цикла... Не знаю даже понятно это или нет!

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

Answer 3

Имеем исходный массив:

$array = [
    '2020-02-07' => ['09:00' => 1500, '10:30' => 1500, '11:00' => 1500, '11:30' => 1500],
    '2020-02-08' => ['09:00' => 1500, '09:30' => 1500, '11:00' => 1500],
];

Пришло в голову сделать следующим образом:

$result = [];
foreach ($array as $k => $v)
{
    $v = array_map(function($a) {
        return strtotime($a);
    }, array_keys($v));
    for ($i = 0; $i < count($v); $i++) {
        $a = $v[$i];
        $b = $a;
        while (isset($v[$i + 1]) && $v[$i + 1] - $v[$i] === 1800) {
            $b = $v[$i + 1];
            $i++;
        }
        $range = $a == $b 
                    ? date('H:i', $a) 
                    : date('H:i', $a) .' - '. date('H:i', $b);
        $result[] = ['date' => $k, 'range' => $range];
    }
}
print_r($result);

https://3v4l.org/oHW1A

А для любителей кода-гольфа, я оставлю следующее (работает только на php7.4+):

$result = [];
$f = fn($p) => date('H:i', $p);
foreach ($array as $k => $v)
{
    $v = array_map(fn($a) => strtotime($a), array_keys($v));
    for ($i = 0; $i < count($v); $i++) {
        [$a, $b] = [$v[$i], &$a];
        while (isset($v[$i + 1]) && $v[$i + 1] - $v[$i] === 1800) [$b = $v[$i + 1], $i++];
        $result[] = ['date' => $k, 'range' => $a == $b ? $f($a) : $f($a).' - '.$f($b)];
    }
}
print_r($result);

https://3v4l.org/qjNjT

READ ALSO
Как без ошибок получить страницу через CURL и отследить редирект?

Как без ошибок получить страницу через CURL и отследить редирект?

Имеется такой код получения страницыПри попытке получить первый url, срабатывает редирект и переменные $error и $response_string пустые

78
Запись массива PHP в БД SQL

Запись массива PHP в БД SQL

Пытаюсь сохранить массив в БД

95
.htaccess, get, чпу

.htaccess, get, чпу

Что нужно прописать в htaccess, что бы превратить ссылку

91
Сделать карту на весь экран

Сделать карту на весь экран

Цель: Сделать карту на весь экран без дублирования по горизонтали и верхнего "пустого пространства"Проблема: Вместо дублирования карты появились...

91