Passed
Push — 3.x ( 148e8f...9015bf )
by Doug
04:46
created

ItemList::top()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 3
eloc 6
c 4
b 1
f 0
nc 4
nop 0
dl 0
loc 12
ccs 6
cts 7
cp 0.8571
crap 3.0261
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 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 60
    public static function fromArray(array $items, bool $preSorted = false): self
62
    {
63 60
        $list = new static();
64 60
        $list->list = array_reverse($items); // internal sort is largest at the end
65 60
        $list->isSorted = $preSorted;
66
67 60
        return $list;
68
    }
69
70 86
    public function insert(Item $item): void
71
    {
72 86
        $this->list[] = $item;
73 86
        $this->isSorted = false;
74 86
        $this->hasConstrainedItems = $this->hasConstrainedItems || $item instanceof ConstrainedPlacementItem;
75 86
    }
76
77
    /**
78
     * Remove item from list.
79
     */
80
    public function remove(Item $item): void
81
    {
82
        if (!$this->isSorted) {
83
            usort($this->list, [$this, 'compare']);
84
            $this->isSorted = true;
85
        }
86
87
        end($this->list);
88
        do {
89
            if (current($this->list) === $item) {
90
                unset($this->list[key($this->list)]);
91
92
                return;
93
            }
94
        } while (prev($this->list) !== false);
95
    }
96
97 58
    public function removePackedItems(PackedItemList $packedItemList): void
98
    {
99 58
        foreach ($packedItemList as $packedItem) {
100 58
            end($this->list);
101
            do {
102 58
                if (current($this->list) === $packedItem->getItem()) {
103 58
                    unset($this->list[key($this->list)]);
104
105 58
                    break;
106
                }
107 10
            } while (prev($this->list) !== false);
108
        }
109 58
    }
110
111
    /**
112
     * @internal
113
     */
114 74
    public function extract(): Item
115
    {
116 74
        if (!$this->isSorted) {
117 34
            usort($this->list, [$this, 'compare']);
118 34
            $this->isSorted = true;
119
        }
120
121 74
        return array_pop($this->list);
122
    }
123
124
    /**
125
     * @internal
126
     */
127 52
    public function top(): Item
128
    {
129 52
        if (!$this->isSorted) {
130 2
            usort($this->list, [$this, 'compare']);
131 2
            $this->isSorted = true;
132
        }
133
134 52
        if (PHP_VERSION_ID < 70300) {
135
            return array_slice($this->list, -1, 1)[0];
136
        }
137
138 52
        return $this->list[array_key_last($this->list)];
139
    }
140
141
    /**
142
     * @internal
143
     * @return ItemList
144
     */
145 36
    public function topN(int $n): self
146
    {
147 36
        if (!$this->isSorted) {
148 2
            usort($this->list, [$this, 'compare']);
149 2
            $this->isSorted = true;
150
        }
151
152 36
        $topNList = new self();
153 36
        $topNList->list = array_slice($this->list, -$n, $n);
154 36
        $topNList->isSorted = true;
155
156 36
        return $topNList;
157
    }
158
159
    /**
160
     * @return Traversable|Item[]
161
     */
162 74
    public function getIterator(): Traversable
163
    {
164 74
        if (!$this->isSorted) {
165 46
            usort($this->list, [$this, 'compare']);
166 46
            $this->isSorted = true;
167
        }
168
169 74
        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 70
    public function hasConstrainedItems(): bool
184
    {
185 70
        if (!isset($this->hasConstrainedItems)) {
186 32
            $this->hasConstrainedItems = false;
187 32
            foreach ($this->list as $item) {
188 32
                if ($item instanceof ConstrainedPlacementItem) {
189 2
                    $this->hasConstrainedItems = true;
190 2
                    break;
191
                }
192
            }
193
        }
194
195 70
        return $this->hasConstrainedItems;
196
    }
197
198 78
    private static function compare(Item $itemA, Item $itemB): int
199
    {
200 78
        $volumeDecider = $itemA->getWidth() * $itemA->getLength() * $itemA->getDepth() <=> $itemB->getWidth() * $itemB->getLength() * $itemB->getDepth();
201 78
        if ($volumeDecider !== 0) {
202 38
            return $volumeDecider;
203
        }
204 68
        $weightDecider = $itemA->getWeight() - $itemB->getWeight();
205 68
        if ($weightDecider !== 0) {
206 2
            return $weightDecider;
207
        }
208
209 66
        return $itemB->getDescription() <=> $itemA->getDescription();
210
    }
211
}
212