While the question was hanging, I decided to quickly write the solution myself.
First of all, I create the Item class with the basic functionality:
class Item { public function __construct($start, $end, $type); public function getStart(); public function getEnd(); public function getType(); }
I decided to call the next class Collection . He will deal with the processing periods:
class Collection { /** * @param Item $main * @param Item[] $items */ public function __construct(Item $main, array $items); }
Inside the constructor, checks are made so that each item from $ items is inside $ main
$main->getStart() <= $item->getStart() && $item->getEnd() <= $main->getEnd()
Then the $ main element is inserted at the beginning of the $ items array and saved.
For the operation of logic, it is not necessary to store the main element separately, but it is advisable to check that all elements are within the main period, so that there is no unexpected behavior.
The first thing to do: break the main period into small ones. That is to collect all the points of the beginnings and ends of all periods.
It is difficult to explain, but the implementation of this logic is as follows:
/** * @return int[] */ public function getPeriodDots() { $dots = []; foreach ($this->items as $item) { /** @var Item $item */ $dots = array_filter($dots, function ($dot) use ($item) { $inPeriod = $item->getStart() <= $dot && $dot <= $item->getEnd(); return !$inPeriod; }); $dots[] = $item->getStart(); $dots[] = $item->getEnd(); } sort($dots); return $dots; }
array_filter needed in order to remove those points that are overridden by another period.
The intervals between points are new periods, the type of which must be determined.
To determine the type of period, it is enough to take the beginning of this period and go through the array of $ items from the bottom up in the search for the parent period to which it belongs. (Still hard to explain)
Here is the implementation of the search type:
/** * @param int $dot * @return int */ public function getTypeForDot($dot) { /** @var Item[] $items */ $items = array_reverse($this->items); $return = $this->items[0]->getType(); foreach ($items as $item) { if ($item->getStart() <= $dot && $dot < $item->getEnd()) { $return = $item->getType(); break; } } return $return; }
PS In general, return could be done inside foreach, but PHPStorm swears that there is not enough return at the end. And if you just leave the stub return $this->items[0]->getType(); or return 0; then we can never test this line and achieve 100% code coverage test.
But creating a new array of periods with the specified types:
/** * @param int[] $dots * @return Item[] */ public function getNewItems($dots) { $startDots = array_slice($dots, 0, -1); $endDots = array_slice($dots, 1); $newItems = []; foreach ($startDots as $i => $start) { $end = $endDots[$i]; $type = $this->getTypeForDot($start); $newItems[] = new Item($start, $end, $type); } return $newItems; }
As a result, we get an array of new elements that go in a row, do not overlap and do not create voids. But...
The problem remains when several types go in a row.
The idea is simple: go through the array and combine periods that are of the same type.
Here is my decision. I think you can do better. If you have ideas, suggest.
/** * @param Item[] $items * @return Item[] */ public function optimizePeriods($items) { $newItems = []; $item = array_shift($items); $start = $item->getStart(); $end = $item->getEnd(); $type = $item->getType(); foreach ($items as $item) { if ($item->getType() === $type) { $end = $item->getEnd(); continue; } $newItems[] = new Item($start, $end, $type); $start = $item->getStart(); $end = $item->getEnd(); $type = $item->getType(); } $newItems[] = new Item($start, $end, $type); return $newItems; }
As a result, you just need to combine all that we have done into one single function:
public function getOptimized() { $dots = $this->getPeriodDots(); $items = $this->getNewItems($dots); return $this->optimizePeriods($items); }
Which returns us an array of elements corresponding to our task.
The result was a neat solution, well tested, independent of the framework.
The complete solution with 100% code coverage is laid out in gist . I decided to hide the namespaces, but the essence does not change.
If it is claimed, I can make a separate library.