DirectedGraph   A
last analyzed

Complexity

Total Complexity 28

Size/Duplication

Total Lines 153
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 153
c 0
b 0
f 0
wmc 28
lcom 1
cbo 1
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A getVertices() 0 4 1
A reindex() 0 8 2
B findAllCycles() 0 21 5
B findAllCyclesRecursive() 0 22 5
C sortAndRemoveDuplicateCycles() 0 28 7
A scrollCycle() 0 16 4
A getDefaultComparator() 0 6 3
1
<?php
2
/**
3
 * @copyright 2018 Aleksander Stelmaczonek <[email protected]>
4
 * @license   MIT License, see license file distributed with this source code
5
 */
6
7
namespace Koriit\PHPDeps\Graph;
8
9
class DirectedGraph
10
{
11
    /** @var Vertex[] */
12
    private $vertices = [];
13
14
    public function __construct(array $vertices)
15
    {
16
        // Remove duplicates
17
        $this->vertices = \array_unique($vertices, SORT_REGULAR);
18
19
        $this->reindex();
20
    }
21
22
    public function getVertices()
23
    {
24
        return $this->vertices;
25
    }
26
27
    /**
28
     * @param callable|null $comparator Comparator of vertex values
29
     *
30
     * @return array
31
     */
32
    public function findAllCycles(callable $comparator = null)
33
    {
34
        $cycles = [];
35
36
        $verticesCount = \count($this->vertices);
37
        foreach ($this->vertices as $vertex) {
38
            $visited = [];
39
            for ($i = 0; $i < $verticesCount; $i++) {
40
                $visited[$i] = false;
41
            }
42
43
            foreach ($vertex->getNeighbours() as $neighbour) {
44
                $foundCycles = $this->findAllCyclesRecursive($vertex, $neighbour, $visited);
45
                if ($foundCycles) {
46
                    $cycles = \array_merge($cycles, $foundCycles);
47
                }
48
            }
49
        }
50
51
        return $this->sortAndRemoveDuplicateCycles($cycles, $comparator);
52
    }
53
54
    /**
55
     * @param Vertex  $needle
56
     * @param Vertex  $current
57
     * @param bool[]  $visited
58
     * @param mixed[] $currentCycle
59
     *
60
     * @return array|bool
61
     */
62
    private function findAllCyclesRecursive(Vertex $needle, Vertex $current, &$visited, $currentCycle = [])
63
    {
64
        if ($visited[$current->getIndex()]) {
65
            return false;
66
        }
67
68
        $currentCycle[] = $current->getValue();
69
        if ($current === $needle) {
70
            return [$currentCycle];
71
        }
72
73
        $cycles = [];
74
        $visited[$current->getIndex()] = true;
75
        foreach ($current->getNeighbours() as $neighbour) {
76
            $foundCycles = $this->findAllCyclesRecursive($needle, $neighbour, $visited, $currentCycle);
77
            if ($foundCycles) {
78
                $cycles = \array_merge($cycles, $foundCycles);
79
            }
80
        }
81
82
        return $cycles;
83
    }
84
85
    private function reindex()
86
    {
87
        $this->vertices = \array_values($this->vertices);
88
        $verticesCount = \count($this->vertices);
89
        for ($i = 0; $i < $verticesCount; $i++) {
90
            $this->vertices[$i]->setIndex($i);
91
        }
92
    }
93
94
    /**
95
     * @param mixed[][]     $cycles     Array of cycles
96
     * @param callable|null $comparator Comparator of vertex values
97
     *
98
     * @return mixed
99
     */
100
    private function sortAndRemoveDuplicateCycles(array $cycles, callable $comparator = null)
101
    {
102
        if ($comparator === null) {
103
            $comparator = $this->getDefaultComparator();
104
        }
105
106
        // Remove duplicates
107
        $cyclesCount = \count($cycles);
108
        for ($i = 0; $i < $cyclesCount; $i++) {
109
            $this->scrollCycle($cycles[$i], $comparator);
110
111
            for ($j = 0; $j < $cyclesCount; $j++) {
112
                if ($i == $j || !isset($cycles[$j])) {
113
                    continue;
114
                }
115
                if ($cycles[$i] === $cycles[$j]) {
116
                    unset($cycles[$i]);
117
                    break;
118
                }
119
            }
120
        }
121
122
        // Sort the cycles themselves
123
        \usort($cycles, $comparator);
124
125
        // Reindex array
126
        return \array_values($cycles);
127
    }
128
129
    /**
130
     * Scrolls a cycle so that minimal element(according to comparator) is at the beginning of cycle.
131
     *
132
     * @param array    $array
133
     * @param callable $comparator
134
     */
135
    private function scrollCycle(array &$array, callable $comparator)
136
    {
137
        $count = \count($array);
138
        // Find minimal element
139
        $firstPos = 0;
140
        for ($i = 1; $i < $count; $i++) {
141
            if ($comparator($array[$firstPos], $array[$i]) === 1) {
142
                $firstPos = $i;
143
            }
144
        }
145
146
        // Push the elements to the end of array until minimal element is at first position
147
        for ($i = 0; $i < $firstPos; $i++) {
148
            \array_push($array, \array_shift($array));
149
        }
150
    }
151
152
    /**
153
     * @return callable
154
     */
155
    private function getDefaultComparator()
156
    {
157
        return function ($a, $b) {
158
            return $a > $b ? 1 : ($a < $b ? -1 : 0);
159
        };
160
    }
161
}
162