Passed
Push — master ( b79dbd...29a054 )
by Doug
08:25 queued 05:38
created

ItemList   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 202
Duplicated Lines 0 %

Test Coverage

Coverage 96.2%

Importance

Changes 9
Bugs 2 Features 0
Metric Value
eloc 70
c 9
b 2
f 0
dl 0
loc 202
ccs 76
cts 79
cp 0.962
rs 9.76
wmc 33

12 Methods

Rating   Name   Duplication   Size   Complexity  
A fromArray() 0 7 1
A removePackedItems() 0 11 4
A top() 0 8 2
A hasConstrainedItems() 0 13 4
A count() 0 3 1
A insert() 0 8 4
A extract() 0 8 2
A hasNoRotationItems() 0 13 4
A getIterator() 0 8 2
A topN() 0 12 2
A compare() 0 12 3
A remove() 0 15 4
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 current;
19
use function end;
20
use IteratorAggregate;
21
use function key;
22
use function prev;
23
use Traversable;
24
use function usort;
25
26
/**
27
 * List of items to be packed, ordered by volume.
28
 *
29
 * @author Doug Wright
30
 */
31
class ItemList implements Countable, IteratorAggregate
32
{
33
    /**
34
     * List containing items.
35
     *
36
     * @var Item[]
37
     */
38
    private $list = [];
39
40
    /**
41
     * Has this list already been sorted?
42
     *
43
     * @var bool
44
     */
45
    private $isSorted = false;
46
47
    /**
48
     * Does this list contain constrained items?
49
     *
50
     * @var ?bool
51
     */
52
    private $hasConstrainedItems;
53
54
    /**
55
     * Does this list contain items which cannot be rotated?
56
     *
57
     * @var ?bool
58
     */
59
    private $hasNoRotationItems;
60
61
    /**
62
     * Do a bulk create.
63
     *
64
     * @param  Item[]   $items
65
     * @return ItemList
66
     */
67 68
    public static function fromArray(array $items, bool $preSorted = false): self
68
    {
69 68
        $list = new static();
70 68
        $list->list = array_reverse($items); // internal sort is largest at the end
71 68
        $list->isSorted = $preSorted;
72
73 68
        return $list;
74
    }
75
76 92
    public function insert(Item $item, int $qty = 1): void
77
    {
78 92
        for ($i = 0; $i < $qty; ++$i) {
79 92
            $this->list[] = $item;
80
        }
81 92
        $this->isSorted = false;
82 92
        $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem;
83 92
        $this->hasNoRotationItems = $this->hasNoRotationItems || $item->getAllowedRotations() <= Item::ROTATION_NEVER;
84 92
    }
85
86
    /**
87
     * Remove item from list.
88
     */
89 8
    public function remove(Item $item): void
90
    {
91 8
        if (!$this->isSorted) {
92 2
            usort($this->list, [$this, 'compare']);
93 2
            $this->isSorted = true;
94
        }
95
96 8
        end($this->list);
97
        do {
98 8
            if (current($this->list) === $item) {
99 8
                unset($this->list[key($this->list)]);
100
101 8
                return;
102
            }
103 2
        } while (prev($this->list) !== false);
104
    }
105
106 66
    public function removePackedItems(PackedItemList $packedItemList): void
107
    {
108 66
        foreach ($packedItemList as $packedItem) {
109 66
            end($this->list);
110
            do {
111 66
                if (current($this->list) === $packedItem->getItem()) {
112 66
                    unset($this->list[key($this->list)]);
113
114 66
                    break;
115
                }
116 16
            } while (prev($this->list) !== false);
117
        }
118 66
    }
119
120
    /**
121
     * @internal
122
     */
123 82
    public function extract(): Item
124
    {
125 82
        if (!$this->isSorted) {
126 34
            usort($this->list, [$this, 'compare']);
127 34
            $this->isSorted = true;
128
        }
129
130 82
        return array_pop($this->list);
131
    }
132
133
    /**
134
     * @internal
135
     */
136 60
    public function top(): Item
137
    {
138 60
        if (!$this->isSorted) {
139 2
            usort($this->list, [$this, 'compare']);
140 2
            $this->isSorted = true;
141
        }
142
143 60
        return $this->list[array_key_last($this->list)];
144
    }
145
146
    /**
147
     * @internal
148
     * @return ItemList
149
     */
150 38
    public function topN(int $n): self
151
    {
152 38
        if (!$this->isSorted) {
153 2
            usort($this->list, [$this, 'compare']);
154 2
            $this->isSorted = true;
155
        }
156
157 38
        $topNList = new self();
158 38
        $topNList->list = array_slice($this->list, -$n, $n);
159 38
        $topNList->isSorted = true;
160
161 38
        return $topNList;
162
    }
163
164
    /**
165
     * @return Traversable<Item>
166
     */
167 80
    public function getIterator(): Traversable
168
    {
169 80
        if (!$this->isSorted) {
170 52
            usort($this->list, [$this, 'compare']);
171 52
            $this->isSorted = true;
172
        }
173
174 80
        return new ArrayIterator(array_reverse($this->list));
175
    }
176
177
    /**
178
     * Number of items in list.
179
     */
180 88
    public function count(): int
181
    {
182 88
        return count($this->list);
183
    }
184
185
    /**
186
     * Does this list contain items with constrained placement criteria.
187
     */
188 78
    public function hasConstrainedItems(): bool
189
    {
190 78
        if (!isset($this->hasConstrainedItems)) {
191 36
            $this->hasConstrainedItems = false;
192 36
            foreach ($this->list as $item) {
193 36
                if ($item instanceof ConstrainedPlacementItem) {
194 2
                    $this->hasConstrainedItems = true;
195 2
                    break;
196
                }
197
            }
198
        }
199
200 78
        return $this->hasConstrainedItems;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->hasConstrainedItems could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
201
    }
202
203
    /**
204
     * Does this list contain items which cannot be rotated.
205
     */
206 78
    public function hasNoRotationItems(): bool
207
    {
208 78
        if (!isset($this->hasNoRotationItems)) {
209 36
            $this->hasNoRotationItems = false;
210 36
            foreach ($this->list as $item) {
211 36
                if ($item->getAllowedRotations() <= Item::ROTATION_NEVER) {
212
                    $this->hasNoRotationItems = true;
213
                    break;
214
                }
215
            }
216
        }
217
218 78
        return $this->hasNoRotationItems;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->hasNoRotationItems could return the type null which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
219
    }
220
221 86
    private static function compare(Item $itemA, Item $itemB): int
222
    {
223 86
        $volumeDecider = $itemA->getWidth() * $itemA->getLength() * $itemA->getDepth() <=> $itemB->getWidth() * $itemB->getLength() * $itemB->getDepth();
224 86
        if ($volumeDecider !== 0) {
225 46
            return $volumeDecider;
226
        }
227 76
        $weightDecider = $itemA->getWeight() - $itemB->getWeight();
228 76
        if ($weightDecider !== 0) {
229 2
            return $weightDecider;
230
        }
231
232 74
        return $itemB->getDescription() <=> $itemA->getDescription();
233
    }
234
}
235