Passed
Push — master ( efb060...599fbf )
by Doug
03:20
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 97.3%

Importance

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

11 Methods

Rating   Name   Duplication   Size   Complexity  
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
A setItems() 0 11 3

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