Passed
Push — master ( efb060...599fbf )
by Doug
03:20
created

WeightRedistributor   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 141
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 62.32%

Importance

Changes 0
Metric Value
wmc 16
lcom 1
cbo 7
dl 0
loc 141
ccs 43
cts 69
cp 0.6232
rs 10
c 0
b 0
f 0

3 Methods

Rating   Name   Duplication   Size   Complexity  
A sortMoreSpaceFirst() 0 12 3
A __construct() 0 5 1
C redistributeWeight() 0 92 12
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
    protected $boxes;
31
32
    /**
33
     * Constructor.
34
     *
35
     * @param BoxList $boxList
36
     */
37 1
    public function __construct(BoxList $boxList)
38
    {
39 1
        $this->boxes = $boxList;
40 1
        $this->logger = new NullLogger();
41 1
    }
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 1
    public function redistributeWeight(PackedBoxList $originalBoxes): PackedBoxList
51
    {
52 1
        $targetWeight = $originalBoxes->getMeanWeight();
53 1
        $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}");
54
55 1
        $packedBoxes = new PackedBoxList();
56
57 1
        $overWeightBoxes = [];
58 1
        $underWeightBoxes = [];
59 1
        foreach ($originalBoxes as $packedBox) {
60 1
            $boxWeight = $packedBox->getWeight();
61 1
            if ($boxWeight > $targetWeight) {
62 1
                $overWeightBoxes[] = $packedBox;
63 1
            } elseif ($boxWeight < $targetWeight) {
64 1
                $underWeightBoxes[] = $packedBox;
65
            } else {
66 1
                $packedBoxes->insert($packedBox); //target weight, so we'll keep these
67
            }
68
        }
69
70
        do { //Keep moving items from most overweight box to most underweight box
71 1
            $tryRepack = false;
72 1
            $this->logger->log(LogLevel::DEBUG, 'boxes under/over target: '.count($underWeightBoxes).'/'.count($overWeightBoxes));
73
74 1
            usort($overWeightBoxes, [$this, 'sortMoreSpaceFirst']);
75 1
            usort($underWeightBoxes, [$this, 'sortMoreSpaceFirst']);
76
77 1
            foreach ($underWeightBoxes as $u => $underWeightBox) {
78 1
                $this->logger->log(LogLevel::DEBUG, 'Underweight Box '.$u);
79 1
                foreach ($overWeightBoxes as $o => $overWeightBox) {
80 1
                    $this->logger->log(LogLevel::DEBUG, 'Overweight Box '.$o);
81 1
                    $overWeightBoxItems = $overWeightBox->getItems()->asItemArray();
82
83
                    //For each item in the heavier box, try and move it to the lighter one
84
                    /** @var Item $overWeightBoxItem */
85 1
                    foreach ($overWeightBoxItems as $oi => $overWeightBoxItem) {
86 1
                        $this->logger->log(LogLevel::DEBUG, 'Overweight Item '.$oi);
87 1
                        if ($underWeightBox->getWeight() + $overWeightBoxItem->getWeight() > $targetWeight) {
88 1
                            $this->logger->log(LogLevel::DEBUG, 'Skipping item for hindering weight distribution');
89 1
                            continue; //skip if moving this item would hinder rather than help weight distribution
90
                        }
91
92
                        $newItemsForLighterBox = $underWeightBox->getItems()->asItemArray();
93
                        $newItemsForLighterBox[] = $overWeightBoxItem;
94
95
                        $newLighterBoxPacker = new Packer(); //we may need a bigger box
96
                        $newLighterBoxPacker->setBoxes($this->boxes);
97
                        $newLighterBoxPacker->setItems($newItemsForLighterBox);
0 ignored issues
show
Documentation introduced by
$newItemsForLighterBox is of type array<integer,?>, 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...
98
                        $this->logger->log(LogLevel::INFO, '[ATTEMPTING TO PACK LIGHTER BOX]');
99
                        $newLighterBox = $newLighterBoxPacker->doVolumePacking()->top();
100
101
                        if ($newLighterBox->getItems()->count() === count($newItemsForLighterBox)) { //new item fits
102
                            $this->logger->log(LogLevel::DEBUG, 'New item fits');
103
                            unset($overWeightBoxItems[$oi]); //now packed in different box
104
105
                            if (count($overWeightBoxItems) > 0) {
106
                                $newHeavierBoxPacker = new Packer(); //we may be able to use a smaller box
107
                                $newHeavierBoxPacker->setBoxes($this->boxes);
108
                                $newHeavierBoxPacker->setItems($overWeightBoxItems);
109
110
                                $this->logger->log(LogLevel::INFO, '[ATTEMPTING TO PACK HEAVIER BOX]');
111
                                $newHeavierBoxes = $newHeavierBoxPacker->doVolumePacking();
112
                                if ($newHeavierBoxes->count()
113
                                    > 1) { //found an edge case in packing algorithm that *increased* box count
114
                                    $this->logger->log(
115
                                        LogLevel::INFO,
116
                                        '[REDISTRIBUTING WEIGHT] Abandoning redistribution, because new packing is less efficient than original'
117
                                    );
118
119
                                    return $originalBoxes;
120
                                }
121
122
                                $overWeightBoxes[$o] = $newHeavierBoxes->top();
123
                            } else {
124
                                unset($overWeightBoxes[$o]);
125
                            }
126
                            $underWeightBoxes[$u] = $newLighterBox;
127
128
                            $tryRepack = true; //we did some work, so see if we can do even better
129 1
                            break 3;
130
                        }
131
                    }
132
                }
133
            }
134 1
        } while ($tryRepack);
135
136
        //Combine back into a single list
137 1
        $packedBoxes->insertFromArray($overWeightBoxes);
138 1
        $packedBoxes->insertFromArray($underWeightBoxes);
139
140 1
        return $packedBoxes;
141
    }
142
143
    /**
144
     * @param PackedBox $boxA
145
     * @param PackedBox $boxB
146
     *
147
     * @return int
148
     */
149 1
    private function sortMoreSpaceFirst(PackedBox $boxA, PackedBox $boxB): int
150
    {
151 1
        $choice = $boxB->getItems()->count() - $boxA->getItems()->count();
152 1
        if ($choice === 0) {
153 1
            $choice = $boxA->getInnerVolume() - $boxB->getInnerVolume();
154
        }
155 1
        if ($choice === 0) {
156 1
            $choice = $boxB->getWeight() - $boxA->getWeight();
157
        }
158
159 1
        return $choice;
160
    }
161
}
162