Completed
Push — issue_187 ( fb1b49...8952f9 )
by Doug
158:46 queued 155:58
created

WeightRedistributor::didRepackActuallyHelp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 4
dl 0
loc 9
ccs 0
cts 6
cp 0
crap 2
rs 10
c 0
b 0
f 0
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_merge;
13
use function count;
14
use function iterator_to_array;
15
use Psr\Log\LoggerAwareInterface;
16
use Psr\Log\LoggerAwareTrait;
17
use Psr\Log\LogLevel;
18
use Psr\Log\NullLogger;
19
use function usort;
20
21
/**
22
 * Actual packer.
23
 *
24
 * @author Doug Wright
25
 * @internal
26
 */
27
class WeightRedistributor implements LoggerAwareInterface
28
{
29
    use LoggerAwareTrait;
30
31
    /**
32
     * List of box sizes available to pack items into.
33
     *
34
     * @var BoxList
35
     */
36
    private $boxes;
37
38
    /**
39
     * Constructor.
40
     */
41 2
    public function __construct(BoxList $boxList)
42
    {
43 2
        $this->boxes = $boxList;
44 2
        $this->logger = new NullLogger();
45 2
    }
46
47
    /**
48
     * Given a solution set of packed boxes, repack them to achieve optimum weight distribution.
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
        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
                        $boxes = array_filter($boxes, function (?PackedBox $box) { //remove any now-empty boxes from the list
74
                            return $box instanceof PackedBox;
75
                        });
76
                        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
     * @return bool was the weight rebalanced?
93
     */
94 2
    private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targetWeight): bool
95
    {
96 2
        $anyIterationSuccessful = false;
97
98 2
        if ($boxA->getWeight() > $boxB->getWeight()) {
99 2
            $overWeightBox = $boxA;
100 2
            $underWeightBox = $boxB;
101
        } else {
102
            $overWeightBox = $boxB;
103
            $underWeightBox = $boxA;
104
        }
105
106 2
        $overWeightBoxItems = $overWeightBox->getItems()->asItemArray();
107 2
        $underWeightBoxItems = $underWeightBox->getItems()->asItemArray();
108
109 2
        foreach ($overWeightBoxItems as $key => $overWeightItem) {
110 2
            if ($overWeightItem->getWeight() + $boxB->getWeight() > $targetWeight) {
111 2
                continue; // moving this item would harm more than help
112
            }
113
114
            $newLighterBoxes = $this->doVolumeRepack(array_merge($underWeightBoxItems, [$overWeightItem]));
115
            if ($newLighterBoxes->count() !== 1) {
116
                continue; //only want to move this item if it still fits in a single box
117
            }
118
119
            $underWeightBoxItems[] = $overWeightItem;
120
121
            if (count($overWeightBoxItems) === 1) { //sometimes a repack can be efficient enough to eliminate a box
122
                $boxB = $newLighterBoxes->top();
123
                $boxA = null;
124
125
                return true;
126
            } else {
127
                unset($overWeightBoxItems[$key]);
128
                $newHeavierBoxes = $this->doVolumeRepack($overWeightBoxItems);
129
                if (count($newHeavierBoxes) !== 1) {
130
                    continue;
131
                }
132
133
                if ($this->didRepackActuallyHelp($boxA, $boxB, $newHeavierBoxes->top(), $newLighterBoxes->top())) {
134
                    $boxB = $newLighterBoxes->top();
135
                    $boxA = $newHeavierBoxes->top();
136
                    $anyIterationSuccessful = true;
137
                }
138
            }
139
        }
140
141 2
        return $anyIterationSuccessful;
142
    }
143
144
    /**
145
     * Do a volume repack of a set of items.
146
     */
147
    private function doVolumeRepack(iterable $items): PackedBoxList
148
    {
149
        $packer = new Packer();
150
        $packer->setBoxes($this->boxes); // use the full set of boxes to allow smaller/larger for full efficiency
151
        $packer->setItems($items);
152
153
        return $packer->doVolumePacking();
154
    }
155
156
    /**
157
     * Not every attempted repack is actually helpful - sometimes moving an item between two otherwise identical
158
     * boxes, or sometimes the box used for the now lighter set of items actually weighs more when empty causing
159
     * an increase in total weight.
160
     */
161
    private function didRepackActuallyHelp(PackedBox $oldBoxA, PackedBox $oldBoxB, PackedBox $newBoxA, PackedBox $newBoxB): bool
162
    {
163
        $oldList = new PackedBoxList();
164
        $oldList->insertFromArray([$oldBoxA, $oldBoxB]);
165
166
        $newList = new PackedBoxList();
167
        $newList->insertFromArray([$newBoxA, $newBoxB]);
168
169
        return $newList->getWeightVariance() < $oldList->getWeightVariance();
170
    }
171
}
172