Passed
Push — 1.x-dev ( 9106e4...cc1b76 )
by Doug
02:13
created

WeightRedistributor::redistributeWeight()   C

Complexity

Conditions 12
Paths 32

Size

Total Lines 92
Code Lines 61

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 24.9738

Importance

Changes 0
Metric Value
dl 0
loc 92
ccs 32
cts 58
cp 0.5517
rs 5.034
c 0
b 0
f 0
cc 12
eloc 61
nc 32
nop 1
crap 24.9738

How to fix   Long Method    Complexity   

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
8
namespace DVDoug\BoxPacker;
9
10
use Psr\Log\LoggerAwareInterface;
11
use Psr\Log\LoggerAwareTrait;
12
use Psr\Log\LogLevel;
13
use Psr\Log\NullLogger;
14
15
/**
16
 * Actual packer.
17
 *
18
 * @author Doug Wright
19
 */
20
class WeightRedistributor implements LoggerAwareInterface
21
{
22
    use LoggerAwareTrait;
23
24
    /**
25
     * List of box sizes available to pack items into.
26
     *
27
     * @var BoxList
28
     */
29
    protected $boxes;
30
31
    /**
32
     * Constructor.
33
     *
34
     * @param BoxList $boxList
35
     */
36 1
    public function __construct(BoxList $boxList)
37
    {
38 1
        $this->boxes = clone $boxList;
39 1
        $this->logger = new NullLogger();
40 1
    }
41
42
    /**
43
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
44
     *
45
     * @param PackedBoxList $originalBoxes
46
     *
47
     * @return PackedBoxList
48
     */
49 1
    public function redistributeWeight(PackedBoxList $originalBoxes)
50
    {
51 1
        $targetWeight = $originalBoxes->getMeanWeight();
52 1
        $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}");
53
54 1
        $packedBoxes = new PackedBoxList();
55
56 1
        $overWeightBoxes = [];
57 1
        $underWeightBoxes = [];
58 1
        foreach (clone $originalBoxes as $packedBox) {
59 1
            $boxWeight = $packedBox->getWeight();
60 1
            if ($boxWeight > $targetWeight) {
61 1
                $overWeightBoxes[] = $packedBox;
62 1
            } elseif ($boxWeight < $targetWeight) {
63 1
                $underWeightBoxes[] = $packedBox;
64
            } else {
65 1
                $packedBoxes->insert($packedBox); //target weight, so we'll keep these
66
            }
67
        }
68
69
        do { //Keep moving items from most overweight box to most underweight box
70 1
            $tryRepack = false;
71 1
            $this->logger->log(LogLevel::DEBUG, 'boxes under/over target: '.count($underWeightBoxes).'/'.count($overWeightBoxes));
72
73 1
            usort($overWeightBoxes, [$this, 'sortMoreSpaceFirst']);
74 1
            usort($underWeightBoxes, [$this, 'sortMoreSpaceFirst']);
75
76 1
            foreach ($underWeightBoxes as $u => $underWeightBox) {
77 1
                $this->logger->log(LogLevel::DEBUG, 'Underweight Box '.$u);
78 1
                foreach ($overWeightBoxes as $o => $overWeightBox) {
79 1
                    $this->logger->log(LogLevel::DEBUG, 'Overweight Box '.$o);
80 1
                    $overWeightBoxItems = $overWeightBox->getItems()->asArray();
81
82
                    //For each item in the heavier box, try and move it to the lighter one
83
                    /** @var Item $overWeightBoxItem */
84 1
                    foreach ($overWeightBoxItems as $oi => $overWeightBoxItem) {
85 1
                        $this->logger->log(LogLevel::DEBUG, 'Overweight Item '.$oi);
86 1
                        if ($underWeightBox->getWeight() + $overWeightBoxItem->getWeight() > $targetWeight) {
87 1
                            $this->logger->log(LogLevel::DEBUG, 'Skipping item for hindering weight distribution');
88 1
                            continue; //skip if moving this item would hinder rather than help weight distribution
89
                        }
90
91
                        $newItemsForLighterBox = $underWeightBox->getItems()->asArray();
92
                        $newItemsForLighterBox[] = $overWeightBoxItem;
93
94
                        $newLighterBoxPacker = new Packer(); //we may need a bigger box
95
                        $newLighterBoxPacker->setBoxes($this->boxes);
96
                        $newLighterBoxPacker->setItems($newItemsForLighterBox);
97
                        $this->logger->log(LogLevel::INFO, '[ATTEMPTING TO PACK LIGHTER BOX]');
98
                        $newLighterBox = $newLighterBoxPacker->doVolumePacking()->top();
99
100
                        if ($newLighterBox->getItems()->count() === count($newItemsForLighterBox)) { //new item fits
101
                            $this->logger->log(LogLevel::DEBUG, 'New item fits');
102
                            unset($overWeightBoxItems[$oi]); //now packed in different box
103
104
                            if (count($overWeightBoxItems) > 0) {
105
                                $newHeavierBoxPacker = new Packer(); //we may be able to use a smaller box
106
                                $newHeavierBoxPacker->setBoxes($this->boxes);
107
                                $newHeavierBoxPacker->setItems($overWeightBoxItems);
108
109
                                $this->logger->log(LogLevel::INFO, '[ATTEMPTING TO PACK HEAVIER BOX]');
110
                                $newHeavierBoxes = $newHeavierBoxPacker->doVolumePacking();
111
                                if ($newHeavierBoxes->count()
112
                                    > 1) { //found an edge case in packing algorithm that *increased* box count
113
                                    $this->logger->log(
114
                                        LogLevel::INFO,
115
                                        '[REDISTRIBUTING WEIGHT] Abandoning redistribution, because new packing is less efficient than original'
116
                                    );
117
118
                                    return $originalBoxes;
119
                                }
120
121
                                $overWeightBoxes[$o] = $newHeavierBoxes->top();
122
                            } else {
123
                                unset($overWeightBoxes[$o]);
124
                            }
125
                            $underWeightBoxes[$u] = $newLighterBox;
126
127
                            $tryRepack = true; //we did some work, so see if we can do even better
128 1
                            break 3;
129
                        }
130
                    }
131
                }
132
            }
133 1
        } while ($tryRepack);
134
135
        //Combine back into a single list
136 1
        $packedBoxes->insertFromArray($overWeightBoxes);
137 1
        $packedBoxes->insertFromArray($underWeightBoxes);
138
139 1
        return $packedBoxes;
140
    }
141
142
    /**
143
     * @param PackedBox $boxA
144
     * @param PackedBox $boxB
145
     *
146
     * @return int
147
     */
148 1 View Code Duplication
    private function sortMoreSpaceFirst(PackedBox $boxA, PackedBox $boxB)
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...
149
    {
150 1
        $choice = $boxB->getItems()->count() - $boxA->getItems()->count();
151 1
        if ($choice === 0) {
152 1
            $choice = $boxA->getInnerVolume() - $boxB->getInnerVolume();
153
        }
154 1
        if ($choice === 0) {
155 1
            $choice = $boxB->getWeight() - $boxA->getWeight();
156
        }
157
158 1
        return $choice;
159
    }
160
}
161