Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
Total | |
100.00% |
1 / 1 |
|
100.00% |
9 / 9 |
CRAP | |
100.00% |
56 / 56 |
GraphSearch | |
100.00% |
1 / 1 |
|
100.00% |
9 / 9 |
26 | |
100.00% |
56 / 56 |
__construct | |
100.00% |
1 / 1 |
1 | |
100.00% |
6 / 6 |
|||
DFS | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
BFS | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
runForward | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
runBackward | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
runBothWays | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
onNode | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
onEdge | |
100.00% |
1 / 1 |
3 | |
100.00% |
2 / 2 |
|||
start | |
100.00% |
1 / 1 |
14 | |
100.00% |
34 / 34 |
<?php declare(strict_types = 1); | |
/* | |
* Copyright (c) 2017, Josef Kufner <josef@kufner.cz> | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
*/ | |
namespace Smalldb\Graph; | |
use Smalldb\StateMachine\InvalidArgumentException; | |
/** | |
* Depth First Search & friends. | |
*/ | |
class GraphSearch | |
{ | |
private NestedGraph $graph; | |
/** @var callable(Node): bool */ | |
private $processNodeCb; | |
/** @var callable(Node): bool */ | |
private $processNodeCbDefault; | |
/** @var callable(Node, Edge, Node, bool): bool */ | |
private $checkEdgeCb; | |
/** @var callable(Node, Edge, Node, bool): bool */ | |
private $checkArrowCbDefault; | |
private int $strategy; | |
private int $direction = self::DIR_FORWARD; | |
const DFS_STRATEGY = 0x01; | |
const BFS_STRATEGY = 0x02; | |
const DIR_FORWARD = 0x10; | |
const DIR_BACKWARD = 0x20; | |
const DIR_BOTH = 0x30; | |
/** | |
* Constructor. | |
*/ | |
private function __construct(NestedGraph $g) | |
{ | |
$this->graph = $g; | |
$this->processNodeCb | |
= $this->processNodeCbDefault | |
= function(Node $current_node) { return true; }; | |
$this->checkEdgeCb | |
= $this->checkArrowCbDefault | |
= function(Node $current_node, Edge $edge, Node $next_node, bool $next_node_seen) { return true; }; | |
} | |
public static function DFS(NestedGraph $g): self | |
{ | |
$gs = new self($g); | |
$gs->strategy = self::DFS_STRATEGY; | |
return $gs; | |
} | |
public static function BFS(NestedGraph $g): self | |
{ | |
$gs = new self($g); | |
$gs->strategy = self::BFS_STRATEGY; | |
return $gs; | |
} | |
public function runForward(): self | |
{ | |
$this->direction = self::DIR_FORWARD; | |
return $this; | |
} | |
public function runBackward(): self | |
{ | |
$this->direction = self::DIR_BACKWARD; | |
return $this; | |
} | |
public function runBothWays(): self | |
{ | |
$this->direction = self::DIR_BOTH; | |
return $this; | |
} | |
/** | |
* Call $func when entering the node. | |
* | |
* @param callable $callback function(Node $current_node) | |
*/ | |
public function onNode(?callable $callback): self | |
{ | |
$this->processNodeCb = $callback ? : $this->processNodeCbDefault; | |
return $this; | |
} | |
/** | |
* Call $func when inspecting next nodes, before enqueuing them. | |
* Nodes are inspected always, even when they have been seen before, | |
* but once seen nodes are not enqueued again. | |
* | |
* @param callable $callback function(Node $current_node, Edge $edge, Node $next_node, bool $next_node_seen) | |
*/ | |
public function onEdge(?callable $callback): self | |
{ | |
$this->checkEdgeCb = $callback ? : $this->checkEdgeCb; | |
return $this; | |
} | |
/** | |
* Start DFS from $startNodes. | |
* | |
* @param Node[] $startNodes List of starting nodes or their IDs. | |
*/ | |
public function start(array $startNodes) | |
{ | |
$queue = []; // Sometimes, it is a stack ;) | |
$seen = []; | |
$processNodeCb = $this->processNodeCb; | |
$checkEdgeCb = $this->checkEdgeCb; | |
// Enqueue nodes as mark them seen | |
foreach ($startNodes as $i => $node) { | |
if ($node instanceof Node) { | |
$id = $node->getId(); | |
$seen[$id] = true; | |
$queue[] = $node; | |
} else { | |
throw new \InvalidArgumentException("Start node ".var_export($i, true)." is not instance of Node."); | |
} | |
} | |
// Process queue | |
while (!empty($queue)) { | |
/** @var Node $currentNode */ | |
// get next node | |
switch ($this->strategy) { | |
case self::DFS_STRATEGY: | |
$currentNode = array_pop($queue); | |
break; | |
case self::BFS_STRATEGY: | |
$currentNode = array_shift($queue); | |
break; | |
default: | |
throw new InvalidArgumentException('Invalid strategy.'); // @codeCoverageIgnore | |
} | |
$seen[$currentNode->getId()] = true; | |
// Process node | |
if (!$processNodeCb($currentNode)) { | |
continue; | |
} | |
// add next nodes to queue | |
$edges = $currentNode->getConnectedEdges(); | |
foreach ($edges as $edge) { | |
if (($this->direction & self::DIR_FORWARD) && $edge->getStart() === $currentNode) { | |
// Forward edges | |
$nextNode = $edge->getEnd(); | |
} else if (($this->direction & self::DIR_BACKWARD) && $edge->getEnd() === $currentNode) { | |
// Backward edges | |
$nextNode = $edge->getStart(); | |
} else { | |
// Ignore edges of undesired direction. | |
continue; | |
} | |
$nextNodeId = $nextNode->getId(); | |
// Check next node whether it is worth processing | |
$next_node_seen = !empty($seen[$nextNodeId]); | |
if ($checkEdgeCb($currentNode, $edge, $nextNode, $next_node_seen) && !$next_node_seen) { | |
$queue[] = $nextNode; | |
} | |
$seen[$nextNodeId] = true; | |
} | |
} | |
} | |
} | |