1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Sugarcrm\UpgradeSpec\Version\Graph; |
4
|
|
|
|
5
|
|
|
use Sugarcrm\UpgradeSpec\Version\Version; |
6
|
|
|
|
7
|
|
|
class Dijkstra |
8
|
|
|
{ |
9
|
|
|
/** |
10
|
|
|
* @var array |
11
|
|
|
*/ |
12
|
|
|
private $graph; |
13
|
|
|
|
14
|
|
|
/** |
15
|
|
|
* Graph constructor. |
16
|
|
|
* |
17
|
|
|
* @param AdjacencyList $adj |
18
|
|
|
*/ |
19
|
|
|
public function __construct(AdjacencyList $adj) |
20
|
|
|
{ |
21
|
|
|
foreach ($adj as $vertex => $list) { |
22
|
|
|
$list = array_map(function (Version $version) use ($vertex) { |
23
|
|
|
return [(string) $version => (new Version($vertex))->isChildOf($version) ? 0 : 1]; |
24
|
|
|
}, iterator_to_array($list)); |
25
|
|
|
|
26
|
|
|
$this->graph[(string) $vertex] = $list ? call_user_func_array('array_merge', $list) : []; |
27
|
|
|
} |
28
|
|
|
} |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @param Version $from |
32
|
|
|
* @param Version $to |
33
|
|
|
* |
34
|
|
|
* @return array |
35
|
|
|
*/ |
36
|
|
|
public function getPath(Version $from, Version $to) |
37
|
|
|
{ |
38
|
|
|
$destinations = $this->getDestinationVertexes($to); |
39
|
|
|
foreach ($this->getOriginVertexes($from) as $origin) { |
40
|
|
|
foreach ($destinations as $destination) { |
41
|
|
|
if ($path = $this->getShortestPath((string) $origin, (string) $destination)) { |
42
|
|
|
return $this->getRealHops($path); |
43
|
|
|
} |
44
|
|
|
} |
45
|
|
|
} |
46
|
|
|
|
47
|
|
|
return []; |
48
|
|
|
} |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* @param $path |
52
|
|
|
* |
53
|
|
|
* @return array |
54
|
|
|
*/ |
55
|
|
|
private function getRealHops($path) |
56
|
|
|
{ |
57
|
|
|
$pairs = []; |
58
|
|
|
|
59
|
|
|
$first = new Version($path[0]); |
60
|
|
|
foreach (range(1, count($path) - 1) as $index) { |
61
|
|
|
$second = new Version($path[$index]); |
62
|
|
|
if ($first->isChildOf($second)) { |
63
|
|
|
$first = $second; |
64
|
|
|
continue; |
65
|
|
|
} |
66
|
|
|
|
67
|
|
|
$pairs[] = [$first, $second]; |
68
|
|
|
|
69
|
|
|
$first = $second; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
return $pairs; |
73
|
|
|
} |
74
|
|
|
|
75
|
|
|
private function getShortestPath($source, $target) |
76
|
|
|
{ |
77
|
|
|
// array of the best estimates of shortest path to each vertex |
78
|
|
|
$bestEstimates = []; |
79
|
|
|
// array of predecessors for each vertex |
80
|
|
|
$predecessors = []; |
81
|
|
|
// queue of all unoptimized vertices |
82
|
|
|
$queue = new \SplPriorityQueue(); |
83
|
|
|
|
84
|
|
|
foreach ($this->graph as $vertex => $list) { |
85
|
|
|
$bestEstimates[$vertex] = \INF; // set initial distance to "infinity" |
86
|
|
|
$predecessors[$vertex] = null; // no known predecessors yet |
87
|
|
|
foreach ($list as $w => $cost) { |
88
|
|
|
// use the edge cost as the priority |
89
|
|
|
$queue->insert($w, $cost); |
90
|
|
|
} |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// initial distance at source is 0 |
94
|
|
|
$bestEstimates[$source] = 0; |
95
|
|
|
|
96
|
|
|
while (!$queue->isEmpty()) { |
97
|
|
|
// extract min cost |
98
|
|
|
$u = $queue->extract(); |
99
|
|
|
if (!empty($this->graph[$u])) { |
100
|
|
|
// "relax" each adjacent vertex |
101
|
|
|
foreach ($this->graph[$u] as $vertex => $cost) { |
102
|
|
|
// alternate route length to adjacent neighbor |
103
|
|
|
$alt = $bestEstimates[$u] + $cost; |
104
|
|
|
/** |
105
|
|
|
* if alternate route is shorter: |
106
|
|
|
* - update minimum length to vertex |
107
|
|
|
* - add neighbor to predecessors for vertex |
108
|
|
|
*/ |
109
|
|
|
if ($alt < $bestEstimates[$vertex]) { |
110
|
|
|
$bestEstimates[$vertex] = $alt; |
111
|
|
|
$predecessors[$vertex] = $u; |
112
|
|
|
|
113
|
|
|
$queue->insert($vertex, $cost); |
114
|
|
|
} |
115
|
|
|
} |
116
|
|
|
} |
117
|
|
|
} |
118
|
|
|
|
119
|
|
|
// we can now find the shortest path using reverse iteration |
120
|
|
|
$path = []; |
121
|
|
|
$u = $target; |
122
|
|
|
while (isset($predecessors[$u]) && $predecessors[$u]) { |
123
|
|
|
$path[] = $u; |
124
|
|
|
$u = $predecessors[$u]; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
if (!$path) { |
|
|
|
|
128
|
|
|
return []; |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
return array_reverse(array_merge($path, [$source])); |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* @param Version $from |
136
|
|
|
* |
137
|
|
|
* @return OrderedList |
|
|
|
|
138
|
|
|
*/ |
139
|
|
|
private function getOriginVertexes(Version $from) |
140
|
|
|
{ |
141
|
|
|
$aliases = $from->getAliases(); |
142
|
|
|
while ($from->isMinor()) { |
143
|
|
|
$from = $from->getParent(); |
|
|
|
|
144
|
|
|
$aliases = $aliases->merge($from->getAliases()); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
return $aliases->filter(function (Version $version) { |
148
|
|
|
return isset($this->graph[(string) $version]); |
149
|
|
|
}); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @param Version $to |
154
|
|
|
* |
155
|
|
|
* @return OrderedList |
|
|
|
|
156
|
|
|
*/ |
157
|
|
|
private function getDestinationVertexes(Version $to) |
158
|
|
|
{ |
159
|
|
|
return $to->getAliases()->filter(function (Version $version) { |
160
|
|
|
return isset($this->graph[(string) $version]); |
161
|
|
|
}); |
162
|
|
|
} |
163
|
|
|
} |
164
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.