Passed
Push — weight_redistributor ( e70ee3...81dfb2 )
by Doug
02:00
created

WeightRedistributor   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 166
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 89.39%

Importance

Changes 0
Metric Value
wmc 18
lcom 1
cbo 8
dl 0
loc 166
ccs 59
cts 66
cp 0.8939
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C redistributeWeight() 0 38 7
C equaliseWeight() 0 49 8
A doVolumeRepack() 0 8 1
A didRepackActuallyHelp() 0 10 1
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 WeightRedistributor implements LoggerAwareInterface
22
{
23
    use LoggerAwareTrait;
24
25
    /**
26
     * List of box sizes available to pack items into.
27
     *
28
     * @var BoxList
29
     */
30
    private $boxes;
31
32
    /**
33
     * Constructor.
34
     *
35
     * @param BoxList $boxList
36
     */
37 2
    public function __construct(BoxList $boxList)
38
    {
39 2
        $this->boxes = $boxList;
40 2
        $this->logger = new NullLogger();
41 2
    }
42
43
    /**
44
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
45
     *
46
     * @param PackedBoxList $originalBoxes
47
     *
48
     * @return PackedBoxList
49
     */
50 2
    public function redistributeWeight(PackedBoxList $originalBoxes): PackedBoxList
51
    {
52 2
        $targetWeight = $originalBoxes->getMeanWeight();
53 2
        $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}");
54
55
        /** @var PackedBox[] $boxes */
56 2
        $boxes = iterator_to_array($originalBoxes);
57
58 2
        usort($boxes, function (PackedBox $boxA, PackedBox $boxB) {
59 2
            return $boxB->getWeight() <=> $boxA->getWeight();
60 2
        });
61
62
        do {
63 2
            $iterationSuccessful = false;
64
65 2
            foreach ($boxes as $a => &$boxA) {
66 2
                foreach ($boxes as $b => &$boxB) {
67 2
                    if ($b <= $a || $boxA->getWeight() === $boxB->getWeight()) {
68 2
                        continue; //no need to evaluate
69
                    }
70
71 2
                    $iterationSuccessful = $this->equaliseWeight($boxA, $boxB, $targetWeight);
72 2
                    if ($iterationSuccessful) {
73 1
                        $boxes = array_filter($boxes, function (?PackedBox $box) { //remove any now-empty boxes from the list
74 1
                            return $box instanceof PackedBox;
75 1
                        });
76 2
                        break 2;
77
                    }
78
                }
79
            }
80 2
        } while ($iterationSuccessful);
81
82
        //Combine back into a single list
83 2
        $packedBoxes = new PackedBoxList();
84 2
        $packedBoxes->insertFromArray($boxes);
85
86 2
        return $packedBoxes;
87
    }
88
89
    /**
90
     * Attempt to equalise weight distribution between 2 boxes.
91
     *
92
     * @param PackedBox $boxA
93
     * @param PackedBox $boxB
94
     * @param float     $targetWeight
95
     *
96
     * @return bool was the weight rebalanced?
97
     */
98 2
    private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targetWeight): bool
99
    {
100 2
        $anyIterationSuccessful = false;
101
102 2
        if ($boxA->getWeight() > $boxB->getWeight()) {
103 2
            $overWeightBox = $boxA;
104 2
            $underWeightBox = $boxB;
105
        } else {
106
            $overWeightBox = $boxB;
107
            $underWeightBox = $boxA;
108
        }
109
110 2
        $overWeightBoxItems = $overWeightBox->getItems()->asItemArray();
111 2
        $underWeightBoxItems = $underWeightBox->getItems()->asItemArray();
112
113 2
        foreach ($overWeightBoxItems as $key => $overWeightItem) {
114 2
            if ($overWeightItem->getWeight() + $boxB->getWeight() > $targetWeight) {
115 2
                continue; // moving this item would harm more than help
116
            }
117
118 1
            $newLighterBoxes = $this->doVolumeRepack(array_merge($underWeightBoxItems, [$overWeightItem]));
0 ignored issues
show
Documentation introduced by
array_merge($underWeight...array($overWeightItem)) is of type array<integer,object<DVD...ug\\BoxPacker\\Item>"}>, but the function expects a object<DVDoug\BoxPacker\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
119 1
            if (count($newLighterBoxes) !== 1) {
120
                continue; //only want to move this item if it still fits in a single box
121
            }
122
123 1
            $underWeightBoxItems[] = $overWeightItem;
124
125 1
            if (count($overWeightBoxItems) === 1) { //sometimes a repack can be efficient enough to eliminate a box
126
                $boxB = $newLighterBoxes->top();
127
                $boxA = null;
128
129
                return true;
130
            } else {
131 1
                unset($overWeightBoxItems[$key]);
132 1
                $newHeavierBoxes = $this->doVolumeRepack($overWeightBoxItems);
0 ignored issues
show
Documentation introduced by
$overWeightBoxItems is of type array<integer,object<DVDoug\BoxPacker\Item>>, but the function expects a object<DVDoug\BoxPacker\iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
133 1
                if (count($newHeavierBoxes) !== 1) {
134
                    continue;
135
                }
136
137 1
                if ($this->didRepackActuallyHelp($boxA, $boxB, $newHeavierBoxes->top(), $newLighterBoxes->top())) {
138 1
                    $boxB = $newLighterBoxes->top();
139 1
                    $boxA = $newHeavierBoxes->top();
140 1
                    $anyIterationSuccessful = true;
141
                }
142
            }
143
        }
144
145 2
        return $anyIterationSuccessful;
146
    }
147
148
    /**
149
     * Do a volume repack of a set of items.
150
     *
151
     * @param iterable $items
152
     *
153
     * @return PackedBoxList
154
     */
155 1
    private function doVolumeRepack(iterable $items): PackedBoxList
156
    {
157 1
        $packer = new Packer();
158 1
        $packer->setBoxes($this->boxes); // use the full set of boxes to allow smaller/larger for full efficiency
159 1
        $packer->setItems($items);
160
161 1
        return $packer->doVolumePacking();
162
    }
163
164
    /**
165
     * Not every attempted repack is actually helpful - sometimes moving an item between two otherwise identical
166
     * boxes, or sometimes the box used for the now lighter set of items actually weighs more when empty causing
167
     * an increase in total weight.
168
     *
169
     * @param PackedBox $oldBoxA
170
     * @param PackedBox $oldBoxB
171
     * @param PackedBox $newBoxA
172
     * @param PackedBox $newBoxB
173
     *
174
     * @return bool
175
     */
176 1
    private function didRepackActuallyHelp(PackedBox $oldBoxA, PackedBox $oldBoxB, PackedBox $newBoxA, PackedBox $newBoxB): bool
177
    {
178 1
        $oldList = new PackedBoxList();
179 1
        $oldList->insertFromArray([$oldBoxA, $oldBoxB]);
180
181 1
        $newList = new PackedBoxList();
182 1
        $newList->insertFromArray([$newBoxA, $newBoxB]);
183
184 1
        return $newList->getWeightVariance() < $oldList->getWeightVariance();
185
    }
186
}
187