Completed
Push — behat ( e1b705 )
by Doug
05:59
created

Packer::setItems()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 5
cts 6
cp 0.8333
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 7
nc 3
nop 1
crap 3.0416
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 Psr\Log\LoggerAwareInterface;
12
use Psr\Log\LoggerAwareTrait;
13
use Psr\Log\LogLevel;
14
use Psr\Log\NullLogger;
15
16
/**
17
 * Actual packer.
18
 *
19
 * @author Doug Wright
20
 */
21
class Packer implements LoggerAwareInterface
22
{
23
    use LoggerAwareTrait;
24
25
    /**
26
     * Number of boxes at which balancing weight is deemed not worth it.
27
     *
28
     * @var int
29
     */
30
    protected $maxBoxesToBalanceWeight = 12;
31
32
    /**
33
     * List of items to be packed.
34
     *
35
     * @var ItemList
36
     */
37
    protected $items;
38
39
    /**
40
     * List of box sizes available to pack items into.
41
     *
42
     * @var BoxList
43
     */
44
    protected $boxes;
45
46
    /**
47
     * Constructor.
48
     */
49 28
    public function __construct()
50
    {
51 28
        $this->items = new ItemList();
52 28
        $this->boxes = new BoxList();
53
54 28
        $this->logger = new NullLogger();
55
    }
56
57
    /**
58
     * Add item to be packed.
59
     *
60
     * @param Item $item
61
     * @param int  $qty
62
     */
63 27
    public function addItem(Item $item, int $qty = 1): void
64
    {
65 27
        for ($i = 0; $i < $qty; $i++) {
66 27
            $this->items->insert($item);
67
        }
68 27
        $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}");
69
    }
70
71
    /**
72
     * Set a list of items all at once.
73
     *
74
     * @param iterable $items
75
     */
76 2
    public function setItems(iterable $items): void
77
    {
78 2
        if ($items instanceof ItemList) {
79
            $this->items = clone $items;
80
        } else {
81 2
            $this->items = new ItemList();
82 2
            foreach ($items as $item) {
83 2
                $this->items->insert($item);
84
            }
85
        }
86
    }
87
88
    /**
89
     * Add box size.
90
     *
91
     * @param Box $box
92
     */
93 26
    public function addBox(Box $box): void
94
    {
95 26
        $this->boxes->insert($box);
96 26
        $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}");
97
    }
98
99
    /**
100
     * Add a pre-prepared set of boxes all at once.
101
     *
102
     * @param BoxList $boxList
103
     */
104 2
    public function setBoxes(BoxList $boxList): void
105
    {
106 2
        $this->boxes = $boxList;
107
    }
108
109
    /**
110
     * Number of boxes at which balancing weight is deemed not worth the extra computation time.
111
     *
112
     * @return int
113
     */
114 1
    public function getMaxBoxesToBalanceWeight(): int
115
    {
116 1
        return $this->maxBoxesToBalanceWeight;
117
    }
118
119
    /**
120
     * Number of boxes at which balancing weight is deemed not worth the extra computation time.
121
     *
122
     * @param int $maxBoxesToBalanceWeight
123
     */
124 2
    public function setMaxBoxesToBalanceWeight(int $maxBoxesToBalanceWeight)
125
    {
126 2
        $this->maxBoxesToBalanceWeight = $maxBoxesToBalanceWeight;
127
    }
128
129
    /**
130
     * Pack items into boxes.
131
     *
132
     * @return PackedBoxList
133
     */
134 27
    public function pack(): PackedBoxList
135
    {
136 27
        $packedBoxes = $this->doVolumePacking();
137
138
        //If we have multiple boxes, try and optimise/even-out weight distribution
139 25
        if ($packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
140 6
            $redistributor = new WeightRedistributor($this->boxes);
141 6
            $redistributor->setLogger($this->logger);
142 6
            $packedBoxes = $redistributor->redistributeWeight($packedBoxes);
143
        }
144
145 25
        $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes");
146
147 25
        return $packedBoxes;
148
    }
149
150
    /**
151
     * Pack items into boxes using the principle of largest volume item first.
152
     *
153
     * @throws ItemTooLargeException
154
     *
155
     * @return PackedBoxList
156
     */
157 27
    public function doVolumePacking(): PackedBoxList
158
    {
159 27
        $packedBoxes = new PackedBoxList();
160
161
        //Keep going until everything packed
162 27
        while ($this->items->count()) {
163 27
            $packedBoxesIteration = [];
164
165
            //Loop through boxes starting with smallest, see what happens
166 27
            foreach ($this->boxes as $box) {
167 26
                $volumePacker = new VolumePacker($box, clone $this->items);
168 26
                $volumePacker->setLogger($this->logger);
169 26
                $packedBox = $volumePacker->pack();
170 26
                if ($packedBox->getItems()->count()) {
171 26
                    $packedBoxesIteration[] = $packedBox;
172
173
                    //Have we found a single box that contains everything?
174 26
                    if ($packedBox->getItems()->count() === $this->items->count()) {
175 26
                        break;
176
                    }
177
                }
178
            }
179
180
            //Check iteration was productive
181 27
            if (!$packedBoxesIteration) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $packedBoxesIteration of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
182 2
                throw new ItemTooLargeException('Item '.$this->items->top()->getDescription().' is too large to fit into any box', $this->items->top());
183
            }
184
185
            //Find best box of iteration, and remove packed items from unpacked list
186 26
            $bestBox = $this->findBestBoxFromIteration($packedBoxesIteration);
187 26
            $unPackedItems = iterator_to_array($this->items, false);
188 26
            foreach ($bestBox->getItems() as $packedItem) {
189 26
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
190 26
                    if ($packedItem->getItem() === $unpackedItem) {
191 26
                        unset($unPackedItems[$unpackedKey]);
192 26
                        break;
193
                    }
194
                }
195
            }
196 26
            $unpackedItemList = new ItemList();
197 26
            foreach ($unPackedItems as $unpackedItem) {
198 8
                $unpackedItemList->insert($unpackedItem);
199
            }
200 26
            $this->items = $unpackedItemList;
201 26
            $packedBoxes->insert($bestBox);
202
        }
203
204 25
        return $packedBoxes;
205
    }
206
207
    /**
208
     * @param PackedBox[] $packedBoxes
209
     *
210
     * @return PackedBox
211
     */
212 26
    private function findBestBoxFromIteration($packedBoxes): PackedBox
213
    {
214 26
        usort($packedBoxes, [$this, 'compare']);
215
216 26
        return array_shift($packedBoxes);
217
    }
218
219
    /**
220
     * @param PackedBox $boxA
221
     * @param PackedBox $boxB
222
     *
223
     * @return int
224
     */
225 3 View Code Duplication
    private static function compare(PackedBox $boxA, PackedBox $boxB): int
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
226
    {
227 3
        $choice = $boxB->getItems()->count() <=> $boxA->getItems()->count();
228 3
        if ($choice === 0) {
229
            $choice = $boxA->getInnerVolume() <=> $boxB->getInnerVolume();
230
        }
231 3
        if ($choice === 0) {
232
            $choice = $boxA->getWeight() <=> $boxB->getWeight();
233
        }
234
235 3
        return $choice;
236
    }
237
}
238