Passed
Push — master ( 8de8ac...4e9f9e )
by Doug
05:17 queued 02:42
created

ItemList::insert()   A

Complexity

Conditions 6
Paths 18

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6.5625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 7
c 1
b 0
f 0
nc 18
nop 2
dl 0
loc 13
ccs 6
cts 8
cp 0.75
crap 6.5625
rs 9.2222
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
class ItemList implements Countable, IteratorAggregate
30
{
31
    /** @var Item[] */
32
    private array $list = [];
33
34
    private bool $isSorted = false;
35
36
    private ItemSorter $sorter;
37
38
    private ?bool $hasConstrainedItems = null;
39
40
    private ?bool $hasNoRotationItems = null;
41
42 99
    public function __construct(?ItemSorter $sorter = null)
43
    {
44 99
        $this->sorter = $sorter ?: new DefaultItemSorter();
45
    }
46
47
    /**
48
     * Do a bulk create.
49
     *
50
     * @param  Item[]   $items
51
     * @return ItemList
52
     */
53 81
    public static function fromArray(array $items, bool $preSorted = false): self
54
    {
55 81
        $list = new self();
56 81
        $list->list = array_reverse($items); // internal sort is largest at the end
57 81
        $list->isSorted = $preSorted;
58
59 81
        return $list;
60
    }
61
62 100
    public function insert(Item $item, int $qty = 1): void
63
    {
64 100
        for ($i = 0; $i < $qty; ++$i) {
65 100
            $this->list[] = $item;
66
        }
67 100
        $this->isSorted = false;
68
69 100
        if (isset($this->hasConstrainedItems)) { // normally lazy evaluated, override if that's already been done
70
            $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem;
71
        }
72
73 100
        if (isset($this->hasNoRotationItems)) { // normally lazy evaluated, override if that's already been done
74
            $this->hasNoRotationItems = $this->hasNoRotationItems || $item->getAllowedRotation() === Rotation::Never;
0 ignored issues
show
Bug introduced by
The type DVDoug\BoxPacker\Rotation was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
75
        }
76
    }
77
78
    /**
79
     * Remove item from list.
80
     */
81 1
    public function remove(Item $item): void
82
    {
83 1
        if (!$this->isSorted) {
84 1
            usort($this->list, [$this->sorter, 'compare']);
85 1
            $this->list = array_reverse($this->list); // internal sort is largest at the end
86 1
            $this->isSorted = true;
87
        }
88
89 1
        end($this->list);
90
        do {
91 1
            if (current($this->list) === $item) {
92 1
                unset($this->list[key($this->list)]);
93
94 1
                return;
95
            }
96 1
        } while (prev($this->list) !== false);
97
    }
98
99 56
    public function removePackedItems(PackedItemList $packedItemList): void
100
    {
101 56
        foreach ($packedItemList as $packedItem) {
102 56
            end($this->list);
103
            do {
104 56
                if (current($this->list) === $packedItem->getItem()) {
105 56
                    unset($this->list[key($this->list)]);
106
107 56
                    break;
108
                }
109 17
            } while (prev($this->list) !== false);
110
        }
111
    }
112
113
    /**
114
     * @internal
115
     */
116 95
    public function extract(): Item
117
    {
118 95
        if (!$this->isSorted) {
119 55
            usort($this->list, [$this->sorter, 'compare']);
120 55
            $this->list = array_reverse($this->list); // internal sort is largest at the end
121 55
            $this->isSorted = true;
122
        }
123
124 95
        return array_pop($this->list);
125
    }
126
127
    /**
128
     * @internal
129
     */
130 55
    public function top(): Item
131
    {
132 55
        if (!$this->isSorted) {
133 2
            usort($this->list, [$this->sorter, 'compare']);
134 2
            $this->list = array_reverse($this->list); // internal sort is largest at the end
135 2
            $this->isSorted = true;
136
        }
137
138 55
        return $this->list[array_key_last($this->list)];
139
    }
140
141
    /**
142
     * @internal
143
     * @return ItemList
144
     */
145 25
    public function topN(int $n): self
146
    {
147 25
        if (!$this->isSorted) {
148 1
            usort($this->list, [$this->sorter, 'compare']);
149 1
            $this->list = array_reverse($this->list); // internal sort is largest at the end
150 1
            $this->isSorted = true;
151
        }
152
153 25
        $topNList = new self();
154 25
        $topNList->list = array_slice($this->list, -$n, $n);
155 25
        $topNList->isSorted = true;
156
157 25
        return $topNList;
158
    }
159
160
    /**
161
     * @return Traversable<Item>
162
     */
163 91
    public function getIterator(): Traversable
164
    {
165 91
        if (!$this->isSorted) {
166 42
            usort($this->list, [$this->sorter, 'compare']);
167 42
            $this->list = array_reverse($this->list); // internal sort is largest at the end
168 42
            $this->isSorted = true;
169
        }
170
171 91
        return new ArrayIterator(array_reverse($this->list));
172
    }
173
174
    /**
175
     * Number of items in list.
176
     */
177 98
    public function count(): int
178
    {
179 98
        return count($this->list);
180
    }
181
182
    /**
183
     * Does this list contain items with constrained placement criteria.
184
     */
185 92
    public function hasConstrainedItems(): bool
186
    {
187 92
        if (!isset($this->hasConstrainedItems)) {
188 92
            $this->hasConstrainedItems = false;
189 92
            foreach ($this->list as $item) {
190 92
                if ($item instanceof ConstrainedPlacementItem) {
191 3
                    $this->hasConstrainedItems = true;
192 3
                    break;
193
                }
194
            }
195
        }
196
197 92
        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...
198
    }
199
200
    /**
201
     * Does this list contain items which cannot be rotated.
202
     */
203 92
    public function hasNoRotationItems(): bool
204
    {
205 92
        if (!isset($this->hasNoRotationItems)) {
206 92
            $this->hasNoRotationItems = false;
207 92
            foreach ($this->list as $item) {
208 92
                if ($item->getAllowedRotation() === Rotation::Never) {
209
                    $this->hasNoRotationItems = true;
210
                    break;
211
                }
212
            }
213
        }
214
215 92
        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...
216
    }
217
}
218