Passed
Push — master ( 73ddcb...629397 )
by Doug
02:34
created

ItemList::hasConstrainedItems()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
c 0
b 0
f 0
nc 4
nop 0
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 4
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
     * Does this list contain items which cannot be rotated?
53
     *
54
     * @var bool
55
     */
56
    private $hasNoRotationItems;
57
58
    /**
59
     * Do a bulk create.
60
     *
61
     * @param  Item[]   $items
62
     * @return ItemList
63
     */
64 73
    public static function fromArray(array $items, bool $preSorted = false): self
65
    {
66 73
        $list = new static();
67 73
        $list->list = array_reverse($items); // internal sort is largest at the end
68 73
        $list->isSorted = $preSorted;
69
70 73
        return $list;
71
    }
72
73 82
    public function insert(Item $item): void
74
    {
75 82
        $this->list[] = $item;
76 82
        $this->isSorted = false;
77 82
        $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem;
78 82
        $this->hasNoRotationItems = $this->hasNoRotationItems || $item->getAllowedRotations() === Item::ROTATION_NEVER;
79 82
    }
80
81
    /**
82
     * Remove item from list.
83
     */
84 3
    public function remove(Item $item): void
85
    {
86 3
        if (!$this->isSorted) {
87 3
            usort($this->list, [$this, 'compare']);
88 3
            $this->isSorted = true;
89
        }
90
91 3
        end($this->list);
92
        do {
93 3
            if (current($this->list) === $item) {
94 3
                unset($this->list[key($this->list)]);
95
96 3
                return;
97
            }
98
        } while (prev($this->list) !== false);
99
    }
100
101 53
    public function removePackedItems(PackedItemList $packedItemList): void
102
    {
103 53
        foreach ($packedItemList as $packedItem) {
104 48
            end($this->list);
105
            do {
106 48
                if (current($this->list) === $packedItem->getItem()) {
107 48
                    unset($this->list[key($this->list)]);
108
109 48
                    break;
110
                }
111 7
            } while (prev($this->list) !== false);
112
        }
113 53
    }
114
115
    /**
116
     * @internal
117
     */
118 75
    public function extract(): Item
119
    {
120 75
        if (!$this->isSorted) {
121 45
            usort($this->list, [$this, 'compare']);
122 45
            $this->isSorted = true;
123
        }
124
125 75
        return array_pop($this->list);
126
    }
127
128
    /**
129
     * @internal
130
     */
131 63
    public function top(): Item
132
    {
133 63
        if (!$this->isSorted) {
134 1
            usort($this->list, [$this, 'compare']);
135 1
            $this->isSorted = true;
136
        }
137
138 63
        return $this->list[array_key_last($this->list)];
139
    }
140
141
    /**
142
     * @internal
143
     * @return ItemList
144
     */
145 59
    public function topN(int $n): self
146
    {
147 59
        if (!$this->isSorted) {
148 1
            usort($this->list, [$this, 'compare']);
149 1
            $this->isSorted = true;
150
        }
151
152 59
        $topNList = new self();
153 59
        $topNList->list = array_slice($this->list, -$n, $n);
154 59
        $topNList->isSorted = true;
155
156 59
        return $topNList;
157
    }
158
159
    /**
160
     * @return Traversable|Item[]
161
     */
162 78
    public function getIterator(): Traversable
163
    {
164 78
        if (!$this->isSorted) {
165 34
            usort($this->list, [$this, 'compare']);
166 34
            $this->isSorted = true;
167
        }
168
169 78
        return new ArrayIterator(array_reverse($this->list));
170
    }
171
172
    /**
173
     * Number of items in list.
174
     */
175 78
    public function count(): int
176
    {
177 78
        return count($this->list);
178
    }
179
180
    /**
181
     * Does this list contain items with constrained placement criteria.
182
     */
183 73
    public function hasConstrainedItems(): bool
184
    {
185 73
        if (!isset($this->hasConstrainedItems)) {
186 40
            $this->hasConstrainedItems = false;
187 40
            foreach ($this->list as $item) {
188 40
                if ($item instanceof ConstrainedPlacementItem) {
189 3
                    $this->hasConstrainedItems = true;
190 3
                    break;
191
                }
192
            }
193
        }
194
195 73
        return $this->hasConstrainedItems;
196
    }
197
198
    /**
199
     * Does this list contain items which cannot be rotated.
200
     */
201 73
    public function hasNoRotationItems(): bool
202
    {
203 73
        if (!isset($this->hasNoRotationItems)) {
204 40
            $this->hasNoRotationItems = false;
205 40
            foreach ($this->list as $item) {
206 40
                if ($item->getAllowedRotations() === Item::ROTATION_NEVER) {
207
                    $this->hasNoRotationItems = true;
208
                    break;
209
                }
210
            }
211
        }
212
213 73
        return $this->hasNoRotationItems;
214
    }
215
216 76
    private static function compare(Item $itemA, Item $itemB): int
217
    {
218 76
        $volumeDecider = $itemA->getWidth() * $itemA->getLength() * $itemA->getDepth() <=> $itemB->getWidth() * $itemB->getLength() * $itemB->getDepth();
219 76
        if ($volumeDecider !== 0) {
220 41
            return $volumeDecider;
221
        }
222 67
        $weightDecider = $itemA->getWeight() - $itemB->getWeight();
223 67
        if ($weightDecider !== 0) {
224 5
            return $weightDecider;
225
        }
226
227 62
        return $itemB->getDescription() <=> $itemA->getDescription();
228
    }
229
}
230