Passed
Push — 3.x ( 539084...e58d28 )
by Doug
02:52
created

ItemList::remove()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

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