Completed
Push — test_jit ( 87859e...ab7b5d )
by Doug
11:43
created

ItemList::topN()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 12
ccs 8
cts 8
cp 1
crap 2
rs 10
1
<?php
2
/**
3
 * Box packing (3D bin packing, knapsack problem).
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace DVDoug\BoxPacker;
10
11
use function array_key_last;
12
use function array_pop;
13
use function array_reverse;
14
use function array_slice;
15
use ArrayIterator;
16
use function count;
17
use Countable;
18
use function end;
19
use IteratorAggregate;
20
use Traversable;
21
use function usort;
22
23
/**
24
 * List of items to be packed, ordered by volume.
25
 *
26
 * @author Doug Wright
27
 */
28
class ItemList implements Countable, IteratorAggregate
29
{
30
    /**
31
     * List containing items.
32
     *
33
     * @var Item[]
34
     */
35
    private $list = [];
36
37
    /**
38
     * Has this list already been sorted?
39
     *
40
     * @var bool
41
     */
42
    private $isSorted = false;
43
44
    /**
45
     * Does this list contain constrained items?
46
     *
47
     * @var bool
48
     */
49
    private $hasConstrainedItems;
50
51
    /**
52
     * Do a bulk create.
53
     *
54
     * @param  Item[]   $items
55
     * @return ItemList
56
     */
57 58
    public static function fromArray(array $items, bool $preSorted = false): self
58
    {
59 58
        $list = new static();
60 58
        $list->list = array_reverse($items); // internal sort is largest at the end
61 58
        $list->isSorted = $preSorted;
62
63 58
        return $list;
64
    }
65
66 66
    public function insert(Item $item): void
67
    {
68 66
        $this->list[] = $item;
69 66
        $this->isSorted = false;
70 66
        $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem;
71 66
    }
72
73
    /**
74
     * Remove item from list.
75
     */
76 3
    public function remove(Item $item): void
77
    {
78 3
        if (!$this->isSorted) {
79 3
            usort($this->list, [$this, 'compare']);
80 3
            $this->isSorted = true;
81
        }
82
83 3
        end($this->list);
84
        do {
85 3
            if (current($this->list) === $item) {
86 3
                unset($this->list[key($this->list)]);
87
88 3
                return;
89
            }
90 1
        } while (prev($this->list) !== false);
91
    }
92
93 53
    public function removePackedItems(PackedItemList $packedItemList): void
94
    {
95 53
        foreach ($packedItemList as $packedItem) {
96 48
            end($this->list);
97
            do {
98 48
                if (current($this->list) === $packedItem->getItem()) {
99 48
                    unset($this->list[key($this->list)]);
100
101 48
                    break;
102
                }
103 9
            } while (prev($this->list) !== false);
104
        }
105 53
    }
106
107
    /**
108
     * @internal
109
     */
110 60
    public function extract(): Item
111
    {
112 60
        if (!$this->isSorted) {
113 30
            usort($this->list, [$this, 'compare']);
114 30
            $this->isSorted = true;
115
        }
116
117 60
        return array_pop($this->list);
118
    }
119
120
    /**
121
     * @internal
122
     */
123 51
    public function top(): Item
124
    {
125 51
        if (!$this->isSorted) {
126 1
            usort($this->list, [$this, 'compare']);
127 1
            $this->isSorted = true;
128
        }
129
130 51
        if (\PHP_VERSION_ID < 70300) {
131
            return array_slice($this->list, -1, 1)[0];
132
        }
133
134 51
        return $this->list[array_key_last($this->list)];
135
    }
136
137
    /**
138
     * @internal
139
     * @return ItemList
140
     */
141 47
    public function topN(int $n): self
142
    {
143 47
        if (!$this->isSorted) {
144 1
            usort($this->list, [$this, 'compare']);
145 1
            $this->isSorted = true;
146
        }
147
148 47
        $topNList = new self();
149 47
        $topNList->list = array_slice($this->list, -$n, $n);
150 47
        $topNList->isSorted = true;
151
152 47
        return $topNList;
153
    }
154
155
    /**
156
     * @return Traversable|Item[]
157
     */
158 63
    public function getIterator(): Traversable
159
    {
160 63
        if (!$this->isSorted) {
161 33
            usort($this->list, [$this, 'compare']);
162 33
            $this->isSorted = true;
163
        }
164
165 63
        return new ArrayIterator(array_reverse($this->list));
166
    }
167
168
    /**
169
     * Number of items in list.
170
     */
171 63
    public function count(): int
172
    {
173 63
        return count($this->list);
174
    }
175
176
    /**
177
     * Does this list contain items with constrained placement criteria.
178
     */
179 58
    public function hasConstrainedItems(): bool
180
    {
181 58
        if (!isset($this->hasConstrainedItems)) {
182 43
            $this->hasConstrainedItems = false;
183 43
            foreach ($this->list as $item) {
184 43
                if ($item instanceof ConstrainedPlacementItem) {
185 2
                    $this->hasConstrainedItems = true;
186 2
                    break;
187
                }
188
            }
189
        }
190
191 58
        return $this->hasConstrainedItems;
192
    }
193
194 61
    private static function compare(Item $itemA, Item $itemB): int
195
    {
196 61
        $volumeDecider = $itemA->getWidth() * $itemA->getLength() * $itemA->getDepth() <=> $itemB->getWidth() * $itemB->getLength() * $itemB->getDepth();
197 61
        if ($volumeDecider !== 0) {
198 31
            return $volumeDecider;
199
        }
200 54
        $weightDecider = $itemA->getWeight() - $itemB->getWeight();
201 54
        if ($weightDecider !== 0) {
202 4
            return $weightDecider;
203
        }
204
205 51
        return $itemB->getDescription() <=> $itemA->getDescription();
206
    }
207
}
208