Passed
Push — master ( 8de8ac...4e9f9e )
by Doug
05:17 queued 02:42
created

WeightRedistributor::equaliseWeight()   B

Complexity

Conditions 7
Paths 12

Size

Total Lines 54
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 7.6383

Importance

Changes 7
Bugs 1 Features 0
Metric Value
cc 7
eloc 35
c 7
b 1
f 0
nc 12
nop 3
dl 0
loc 54
ccs 26
cts 34
cp 0.7647
crap 7.6383
rs 8.4266

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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_filter;
12
use function array_map;
13
use function array_merge;
14
use function array_sum;
15
use function assert;
16
use function count;
17
use function iterator_to_array;
18
use Psr\Log\LoggerAwareInterface;
19
use Psr\Log\LoggerInterface;
20
use Psr\Log\LogLevel;
21
use Psr\Log\NullLogger;
22
use function usort;
23
use WeakMap;
24
25
/**
26
 * Actual packer.
27
 * @internal
28
 */
29
class WeightRedistributor implements LoggerAwareInterface
30
{
31
    private LoggerInterface $logger;
32
33
    private BoxList $boxes;
34
35
    /** @var WeakMap<Box, int> */
36
    private WeakMap $boxQuantitiesAvailable;
37
38
    private PackedBoxSorter $packedBoxSorter;
39
40
    /**
41
     * @param WeakMap<Box, int> $boxQuantitiesAvailable
42
     */
43 13
    public function __construct(BoxList $boxList, PackedBoxSorter $packedBoxSorter, WeakMap $boxQuantitiesAvailable)
44
    {
45 13
        $this->boxes = $boxList;
46 13
        $this->packedBoxSorter = $packedBoxSorter;
47 13
        $this->boxQuantitiesAvailable = $boxQuantitiesAvailable;
48 13
        $this->logger = new NullLogger();
49
    }
50
51 13
    public function setLogger(LoggerInterface $logger): void
52
    {
53 13
        $this->logger = $logger;
54
    }
55
56
    /**
57
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
58
     */
59 13
    public function redistributeWeight(PackedBoxList $originalBoxes): PackedBoxList
60
    {
61 13
        $targetWeight = $originalBoxes->getMeanItemWeight();
62 13
        $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}");
63
64 13
        $boxes = iterator_to_array($originalBoxes);
65
66 13
        usort($boxes, static fn (PackedBox $boxA, PackedBox $boxB) => $boxB->getWeight() <=> $boxA->getWeight());
67
68
        do {
69 13
            $iterationSuccessful = false;
70
71 13
            foreach ($boxes as $a => &$boxA) {
72 13
                foreach ($boxes as $b => &$boxB) {
73 13
                    if ($b <= $a || $boxA->getWeight() === $boxB->getWeight()) {
74 13
                        continue; // no need to evaluate
75
                    }
76
77 9
                    $iterationSuccessful = $this->equaliseWeight($boxA, $boxB, $targetWeight);
78 9
                    if ($iterationSuccessful) {
79 5
                        $boxes = array_filter($boxes, static function (?PackedBox $box) { // remove any now-empty boxes from the list
80 5
                            return $box instanceof PackedBox;
81
                        });
82 5
                        break 2;
83
                    }
84
                }
85
            }
86
        } while ($iterationSuccessful);
87
88
        // Combine back into a single list
89 13
        $packedBoxes = new PackedBoxList($this->packedBoxSorter);
90 13
        $packedBoxes->insertFromArray($boxes);
91
92 13
        return $packedBoxes;
93
    }
94
95
    /**
96
     * Attempt to equalise weight distribution between 2 boxes.
97
     *
98
     * @return bool was the weight rebalanced?
99
     */
100 9
    private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targetWeight): bool
101
    {
102 9
        $anyIterationSuccessful = false;
103
104 9
        if ($boxA->getWeight() > $boxB->getWeight()) {
105 9
            $overWeightBox = $boxA;
106 9
            $underWeightBox = $boxB;
107
        } else {
108
            $overWeightBox = $boxB;
109
            $underWeightBox = $boxA;
110
        }
111
112 9
        $overWeightBoxItems = $overWeightBox->getItems()->asItemArray();
113 9
        $underWeightBoxItems = $underWeightBox->getItems()->asItemArray();
114
115 9
        foreach ($overWeightBoxItems as $key => $overWeightItem) {
116 9
            if (!self::wouldRepackActuallyHelp($overWeightBoxItems, $overWeightItem, $underWeightBoxItems, $targetWeight)) {
117 8
                continue; // moving this item would harm more than help
118
            }
119
120 6
            $newLighterBoxes = $this->doVolumeRepack(array_merge($underWeightBoxItems, [$overWeightItem]), $underWeightBox->getBox());
121 6
            if ($newLighterBoxes->count() !== 1) {
122 2
                continue; // only want to move this item if it still fits in a single box
123
            }
124
125 5
            $underWeightBoxItems[] = $overWeightItem;
126
127 5
            if (count($overWeightBoxItems) === 1) { // sometimes a repack can be efficient enough to eliminate a box
128
                $boxB = $newLighterBoxes->top();
129
                $boxA = null;
130
                --$this->boxQuantitiesAvailable[$underWeightBox->getBox()];
131
                ++$this->boxQuantitiesAvailable[$overWeightBox->getBox()];
132
133
                return true;
134
            }
135
136 5
            unset($overWeightBoxItems[$key]);
137 5
            $newHeavierBoxes = $this->doVolumeRepack($overWeightBoxItems, $overWeightBox->getBox());
138 5
            if (count($newHeavierBoxes) !== 1) {
139
                assert(true, 'Could not pack n-1 items into box, even though n were previously in it');
140
                continue;
141
            }
142
143 5
            ++$this->boxQuantitiesAvailable[$overWeightBox->getBox()];
144 5
            ++$this->boxQuantitiesAvailable[$underWeightBox->getBox()];
145 5
            --$this->boxQuantitiesAvailable[$newHeavierBoxes->top()->getBox()];
146 5
            --$this->boxQuantitiesAvailable[$newLighterBoxes->top()->getBox()];
147 5
            $underWeightBox = $boxB = $newLighterBoxes->top();
148 5
            $overWeightBox = $boxA = $newHeavierBoxes->top();
149
150 5
            $anyIterationSuccessful = true;
151
        }
152
153 9
        return $anyIterationSuccessful;
154
    }
155
156
    /**
157
     * Do a volume repack of a set of items.
158
     * @param iterable<Item> $items
159
     */
160 6
    private function doVolumeRepack(iterable $items, Box $currentBox): PackedBoxList
161
    {
162 6
        $packer = new Packer();
163 6
        $packer->throwOnUnpackableItem(false);
164 6
        $packer->setBoxes($this->boxes); // use the full set of boxes to allow smaller/larger for full efficiency
165 6
        foreach ($this->boxes as $box) {
166 6
            $packer->setBoxQuantity($box, $this->boxQuantitiesAvailable[$box]);
167
        }
168 6
        $packer->setBoxQuantity($currentBox, $this->boxQuantitiesAvailable[$currentBox] + 1);
169 6
        $packer->setItems($items);
170
171 6
        return $packer->doBasicPacking(true);
172
    }
173
174
    /**
175
     * Not every attempted repack is actually helpful - sometimes moving an item between two otherwise identical
176
     * boxes, or sometimes the box used for the now lighter set of items actually weighs more when empty causing
177
     * an increase in total weight.
178
     * @param array<Item> $overWeightBoxItems
179
     * @param array<Item> $underWeightBoxItems
180
     */
181 9
    private static function wouldRepackActuallyHelp(array $overWeightBoxItems, Item $overWeightItem, array $underWeightBoxItems, float $targetWeight): bool
182
    {
183 9
        $overWeightItemsWeight = array_sum(array_map(static fn (Item $item) => $item->getWeight(), $overWeightBoxItems));
184 9
        $underWeightItemsWeight = array_sum(array_map(static fn (Item $item) => $item->getWeight(), $underWeightBoxItems));
185
186 9
        if ($overWeightItem->getWeight() + $underWeightItemsWeight > $targetWeight) {
187 8
            return false;
188
        }
189
190 6
        $oldVariance = self::calculateVariance($overWeightItemsWeight, $underWeightItemsWeight);
0 ignored issues
show
Bug introduced by
It seems like $overWeightItemsWeight can also be of type double; however, parameter $boxAWeight of DVDoug\BoxPacker\WeightR...or::calculateVariance() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
        $oldVariance = self::calculateVariance(/** @scrutinizer ignore-type */ $overWeightItemsWeight, $underWeightItemsWeight);
Loading history...
Bug introduced by
It seems like $underWeightItemsWeight can also be of type double; however, parameter $boxBWeight of DVDoug\BoxPacker\WeightR...or::calculateVariance() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

190
        $oldVariance = self::calculateVariance($overWeightItemsWeight, /** @scrutinizer ignore-type */ $underWeightItemsWeight);
Loading history...
191 6
        $newVariance = self::calculateVariance($overWeightItemsWeight - $overWeightItem->getWeight(), $underWeightItemsWeight + $overWeightItem->getWeight());
192
193 6
        return $newVariance < $oldVariance;
194
    }
195
196 6
    private static function calculateVariance(int $boxAWeight, int $boxBWeight): float
197
    {
198 6
        return ($boxAWeight - (($boxAWeight + $boxBWeight) / 2)) ** 2; // don't need to calculate B and ÷ 2, for a 2-item population the difference from mean is the same for each box
199
    }
200
}
201