Completed
Pull Request — master (#54)
by
unknown
32:41
created

WeightRedistributor   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 109
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 96.49%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 13
c 2
b 0
f 0
lcom 1
cbo 5
dl 0
loc 109
ccs 55
cts 57
cp 0.9649
rs 10

2 Methods

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