Passed
Push — behat ( a74de5...4872f1 )
by Doug
02:39
created

Packer   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 217
Duplicated Lines 5.07 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 95.95%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 13
dl 11
loc 217
ccs 71
cts 74
cp 0.9595
rs 10
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setItems() 0 11 3
A getMaxBoxesToBalanceWeight() 0 4 1
A pack() 0 15 3
A findBestBoxFromIteration() 0 6 1
A compare() 11 12 3
C doVolumePacking() 0 49 10
A __construct() 0 7 1
A addItem() 0 7 2
A addBox() 0 5 1
A setBoxes() 0 4 1
A setMaxBoxesToBalanceWeight() 0 4 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

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 18
    public function __construct()
50
    {
51 18
        $this->items = new ItemList();
52 18
        $this->boxes = new BoxList();
53
54 18
        $this->logger = new NullLogger();
55 18
    }
56
57
    /**
58
     * Add item to be packed.
59
     *
60
     * @param Item $item
61
     * @param int  $qty
62
     */
63 17
    public function addItem(Item $item, int $qty = 1): void
64
    {
65 17
        for ($i = 0; $i < $qty; $i++) {
66 17
            $this->items->insert($item);
67
        }
68 17
        $this->logger->log(LogLevel::INFO, "added {$qty} x {$item->getDescription()}");
69 17
    }
70
71
    /**
72
     * Set a list of items all at once.
73
     *
74
     * @param iterable $items
75
     */
76 1
    public function setItems(iterable $items): void
77
    {
78 1
        if ($items instanceof ItemList) {
79
            $this->items = clone $items;
80
        } else {
81 1
            $this->items = new ItemList();
82 1
            foreach ($items as $item) {
83 1
                $this->items->insert($item);
84
            }
85
        }
86 1
    }
87
88
    /**
89
     * Add box size.
90
     *
91
     * @param Box $box
92
     */
93 16
    public function addBox(Box $box): void
94
    {
95 16
        $this->boxes->insert($box);
96 16
        $this->logger->log(LogLevel::INFO, "added box {$box->getReference()}");
97 16
    }
98
99
    /**
100
     * Add a pre-prepared set of boxes all at once.
101
     *
102
     * @param BoxList $boxList
103
     */
104 1
    public function setBoxes(BoxList $boxList): void
105
    {
106 1
        $this->boxes = $boxList;
107 1
    }
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 2
    }
128
129
    /**
130
     * Pack items into boxes.
131
     *
132
     * @return PackedBoxList
133
     */
134 17
    public function pack(): PackedBoxList
135
    {
136 17
        $packedBoxes = $this->doVolumePacking();
137
138
        //If we have multiple boxes, try and optimise/even-out weight distribution
139 15
        if ($packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
140 3
            $redistributor = new WeightRedistributor($this->boxes);
141 3
            $redistributor->setLogger($this->logger);
142 3
            $packedBoxes = $redistributor->redistributeWeight($packedBoxes);
143
        }
144
145 15
        $this->logger->log(LogLevel::INFO, "packing completed, {$packedBoxes->count()} boxes");
146
147 15
        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 17
    public function doVolumePacking(): PackedBoxList
158
    {
159 17
        $packedBoxes = new PackedBoxList();
160
161
        //Keep going until everything packed
162 17
        while ($this->items->count()) {
163 17
            $packedBoxesIteration = [];
164
165
            //Loop through boxes starting with smallest, see what happens
166 17
            foreach ($this->boxes as $box) {
167 16
                $volumePacker = new VolumePacker($box, clone $this->items);
168 16
                $volumePacker->setLogger($this->logger);
169 16
                $packedBox = $volumePacker->pack();
170 16
                if ($packedBox->getItems()->count()) {
171 16
                    $packedBoxesIteration[] = $packedBox;
172
173
                    //Have we found a single box that contains everything?
174 16
                    if ($packedBox->getItems()->count() === $this->items->count()) {
175 16
                        break;
176
                    }
177
                }
178
            }
179
180
            //Check iteration was productive
181 17
            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 16
            $bestBox = $this->findBestBoxFromIteration($packedBoxesIteration);
187 16
            $unPackedItems = iterator_to_array($this->items, false);
188 16
            foreach ($bestBox->getItems() as $packedItem) {
189 16
                foreach ($unPackedItems as $unpackedKey => $unpackedItem) {
190 16
                    if ($packedItem->getItem() === $unpackedItem) {
191 16
                        unset($unPackedItems[$unpackedKey]);
192 16
                        break;
193
                    }
194
                }
195
            }
196 16
            $unpackedItemList = new ItemList();
197 16
            foreach ($unPackedItems as $unpackedItem) {
198 5
                $unpackedItemList->insert($unpackedItem);
199
            }
200 16
            $this->items = $unpackedItemList;
201 16
            $packedBoxes->insert($bestBox);
202
        }
203
204 15
        return $packedBoxes;
205
    }
206
207
    /**
208
     * @param PackedBox[] $packedBoxes
209
     *
210
     * @return PackedBox
211
     */
212 16
    private function findBestBoxFromIteration($packedBoxes): PackedBox
213
    {
214 16
        usort($packedBoxes, [$this, 'compare']);
215
216 16
        return array_shift($packedBoxes);
217
    }
218
219
    /**
220
     * @param PackedBox $boxA
221
     * @param PackedBox $boxB
222
     *
223
     * @return int
224
     */
225 1 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 1
        $choice = $boxB->getItems()->count() <=> $boxA->getItems()->count();
228 1
        if ($choice === 0) {
229
            $choice = $boxA->getInnerVolume() <=> $boxB->getInnerVolume();
230
        }
231 1
        if ($choice === 0) {
232
            $choice = $boxA->getWeight() <=> $boxB->getWeight();
233
        }
234
235 1
        return $choice;
236
    }
237
}
238