ItemList::fromArray()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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