From 522db1d462dcabb5d9f6f346b3d7a8fd4517d255 Mon Sep 17 00:00:00 2001 From: Dennis Eichhorn Date: Sat, 17 Jul 2021 21:12:01 +0200 Subject: [PATCH] fix bugs and start impl. additional functions --- DataStorage/Database/DataMapperAbstract.php | 6 +- Message/ResponseAbstract.php | 6 + Stdlib/Graph/Edge.php | 4 +- Stdlib/Graph/Graph.php | 202 +++++++++++++++----- Stdlib/Graph/Node.php | 20 ++ tests/Message/ResponseAbstractTest.php | 2 + tests/Stdlib/Graph/EdgeTest.php | 6 +- tests/Stdlib/Graph/GraphTest.php | 3 - tests/System/File/FileUtilsTest.php | 3 +- 9 files changed, 196 insertions(+), 56 deletions(-) diff --git a/DataStorage/Database/DataMapperAbstract.php b/DataStorage/Database/DataMapperAbstract.php index 41f7cd163..9486658dc 100644 --- a/DataStorage/Database/DataMapperAbstract.php +++ b/DataStorage/Database/DataMapperAbstract.php @@ -416,11 +416,11 @@ class DataMapperAbstract implements DataMapperInterface } if ($condValue['value'] !== null) { - $where1->andWhere(static::$table . '_' . $searchDepth . '.' . $column, $condValue['comparison'], $condValue['value']); + $where1->andWhere(static::$table . '_d' . $searchDepth . '.' . $column, $condValue['comparison'], $condValue['value']); } if ($condValue['orderBy'] !== null) { - $where1->orderBy(static::$table . '_' . $searchDepth . '.' . static::getColumnByMember($condValue['orderBy']), $condValue['sortOrder']); + $where1->orderBy(static::$table . '_d' . $searchDepth . '.' . static::getColumnByMember($condValue['orderBy']), $condValue['sortOrder']); } if ($condValue['limit'] !== null) { @@ -433,7 +433,7 @@ class DataMapperAbstract implements DataMapperInterface $hasAutocompletes = false; foreach (static::$columns as $col) { if (isset($col['autocomplete']) && $col['autocomplete']) { - $where2->where(static::$table . '_' . $searchDepth . '.' . $col['name'], 'LIKE', '%' . $search . '%', 'OR'); + $where2->where(static::$table . '_d' . $searchDepth . '.' . $col['name'], 'LIKE', '%' . $search . '%', 'OR'); $hasAutocompletes = true; } } diff --git a/Message/ResponseAbstract.php b/Message/ResponseAbstract.php index f7fd7f711..2c0d3704d 100644 --- a/Message/ResponseAbstract.php +++ b/Message/ResponseAbstract.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace phpOMS\Message; +use phpOMS\Localization\ISO639x1Enum; + /** * Response abstract class. * @@ -100,6 +102,10 @@ abstract class ResponseAbstract implements \JsonSerializable, MessageInterface */ public function getLanguage() : string { + if (!isset($this->header)) { + return ISO639x1Enum::_EN; + } + return $this->header->l11n->getLanguage(); } diff --git a/Stdlib/Graph/Edge.php b/Stdlib/Graph/Edge.php index 6651a5192..1fefadde6 100644 --- a/Stdlib/Graph/Edge.php +++ b/Stdlib/Graph/Edge.php @@ -58,7 +58,7 @@ class Edge * @var float * @since 1.0.0 */ - private float $weight = 0.0; + private float $weight = 1.0; /** * Constructor. @@ -70,7 +70,7 @@ class Edge * * @since 1.0.0 */ - public function __construct(Node $node1, Node $node2, float $weight = 0.0, bool $isDirected = false) + public function __construct(Node $node1, Node $node2, float $weight = 1.0, bool $isDirected = false) { $this->node1 = $node1; $this->node2 = $node2; diff --git a/Stdlib/Graph/Graph.php b/Stdlib/Graph/Graph.php index 26bcc3a25..7fc582fc8 100644 --- a/Stdlib/Graph/Graph.php +++ b/Stdlib/Graph/Graph.php @@ -21,29 +21,6 @@ namespace phpOMS\Stdlib\Graph; * @license OMS License 1.0 * @link https://orange-management.org * @since 1.0.0 - * - * @todo Orange-Management/phpOMS#10 - * * Count all paths between 2 nodes - * * Return all paths between 2 nodes - * * Find cycles using graph coloring - * * Find a negative cycle - * * Find cycles with n length - * * Find cycles with odd length - * * Find shortest path between 2 nodes - * * Find longest path between 2 nodes - * * Find islands - * * Find all unreachable nodes - * * Check if strongly connected - * * Find longest path between 2 nodes - * * Find longest path - * * Get the girth - * * Get the circuit rank - * * Get the node connectivity - * * Get the edge connectivity - * * Is the graph connected - * * Get the unconnected nodes as their own graph - * * Check if bipartite - * * Check if triangle free */ class Graph { @@ -511,15 +488,79 @@ class Graph } /** - * Perform depth first traversal. + * Perform depth first traversal + * + * @param int|string|Node $node1 Graph node + * @param int|string|Node $node2 Graph node * * @return array * * @since 1.0.0 */ - public function depthFirstTraversal() : array + private function depthFirstTraversal( + int | string | Node $node1, + int | string | Node $node2 = null, + array &$visited, + array &$path, + array &$paths + ) : array { - return []; + $visited[$node1->getId()] = true; + + if ($node1->isEquals($node2)) { + $paths[] = $path; + } + + $neighbors = $node1->getNeighbors(); + foreach ($neighbors as $neighbor) { + if (!isset($visited[$neighbor->getId()]) || !$visited[$neighbor->getId()]) { + $path[] = $neighbor; + $this->depthFirstTraversal($neighbor, $node2, $visited, $path); + \array_pop($path); + } + } + + $visited[$node1->getId()] = false; + } + + /** + * Find all reachable nodes with depth first traversal (iterative) + * + * @param int|string|Node $node1 Graph node + * + * @return Node[] + * + * @since 1.0.0 + */ + public function findAllReachableNodesDFS(int | string | Node $node1) : array + { + $nodes = []; + + if (!($node1 instanceof Node)) { + $node1 = $this->getNode($node1); + } + + $visited = []; + $stack = []; + $stack[] = $node1; + + while (!empty($stack)) { + $cNode = \array_pop($stack); + $nodes[] = $cNode; + + if (!isset($visited[$cNode->getId()]) || !$visited[$cNode->getId()]) { + $visited[$cNode->getId()] = true; + } + + $neighbors = $cNode->getNeighbors(); + foreach ($neighbors as $neighbor) { + if (!isset($visited[$cNode->getId()]) || !$visited[$cNode->getId()]) { + $stack[] = $neighbor; + } + } + } + + return $nodes; } /** @@ -546,6 +587,54 @@ class Graph return []; } + /** + * Get all paths between two nodes + * + * @param int|string|Node $node1 Graph node + * @param int|string|Node $node2 Graph node + * + * @return array + * + * @since 1.0.0 + */ + public function getAllPathsBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array + { + if (!($node1 instanceof Node)) { + $node1 = $this->getNode($node1); + } + + if (!($node2 instanceof Node)) { + $node2 = $this->getNode($node2); + } + + if ($node1 === null || $node2 === null) { + return []; + } + + $path = []; + $paths = []; + $visited = []; + + $this->depthFirstTraversal($node1, $node2, $visited, $path, $paths); + + return $paths; + } + + /** + * Count possible paths between two nodes. + * + * @param int|string|Node $node1 Graph node + * @param int|string|Node $node2 Graph node + * + * @return int + * + * @since 1.0.0 + */ + public function countAllPathsBetweenNodes(int | string | Node $node1, int | string | Node $node2) : int + { + return \count($this->getAllPathsBetweenNodes($node1, $node2)); + } + /** * Get longest path between two nodes. * @@ -558,15 +647,48 @@ class Graph */ public function longestPathBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array { - if (!($node1 instanceof Node)) { - $node1 = $this->getNode($node1); + $paths = $this->getAllPathsBetweenNodes($node1, $node2); + + if (empty($paths)) { + return []; } - if (!($node2 instanceof Node)) { - $node2 = $this->getNode($node2); + foreach ($paths as $key => $path) { + $edges[$key] = 0; + foreach ($path as $node) { + $edges[$key] += $node->getEdgeByNeighbor()->getWeight(); + } } - return []; + \arsort($edges); + + return $paths[\array_key_first($edges)]; + } + + /** + * Get shortest path between two nodes. + * + * @param int|string|Node $node1 Graph node + * @param int|string|Node $node2 Graph node + * + * @return Node[] + * + * @since 1.0.0 + */ + public function shortestPathBetweenNodes(int | string | Node $node1, int | string | Node $node2) : array + { + $paths = $this->getAllPathsBetweenNodes($node1, $node2); + + foreach ($paths as $key => $path) { + $edges[$key] = 0; + foreach ($path as $node) { + $edges[$key] += $node->getEdgeByNeighbor()->getWeight(); + } + } + + \asort($edges); + + return $paths[\array_key_first($edges)]; } /** @@ -684,21 +806,13 @@ class Graph */ public function isConnected() : bool { - return true; - } + if (empty($this->nodes)) { + return true; + } - /** - * Get unconnected sub graphs - * - * @return Graph[] - * - * @since 1.0.0 - */ - public function getUnconnected() : array - { - // get all unconnected sub graphs + $nodes = $this->findAllReachableNodesDFS(\reset($this->nodes)); - return []; + return \count($nodes) === \count($this->nodes); } /** diff --git a/Stdlib/Graph/Node.php b/Stdlib/Graph/Node.php index fa0122aeb..b3c18e8a2 100644 --- a/Stdlib/Graph/Node.php +++ b/Stdlib/Graph/Node.php @@ -172,6 +172,26 @@ class Node return $this->edges[$key] ?? null; } + /** + * Get graph edge by neighbor. + * + * @param Node $node Neighbor node + * + * @return null|Edge + * + * @since 1.0.0 + */ + public function getEdgeByNeighbor(self $node) : ?Edge + { + foreach ($this->edges as $edge) { + if ($edge->getNode1()->isEqual($node) || $edge->getNode2()->isEqual($node)) { + return $edge; + } + } + + return null; + } + /** * Get graph edges * diff --git a/tests/Message/ResponseAbstractTest.php b/tests/Message/ResponseAbstractTest.php index be5e168c9..dd1785171 100644 --- a/tests/Message/ResponseAbstractTest.php +++ b/tests/Message/ResponseAbstractTest.php @@ -17,6 +17,7 @@ namespace phpOMS\tests\Message; require_once __DIR__ . '/../Autoloader.php'; use phpOMS\Message\ResponseAbstract; +use phpOMS\Localization\ISO639x1Enum; /** * @testdox phpOMS\tests\Message\ResponseAbstractTest: Abstract response @@ -52,6 +53,7 @@ class ResponseAbstractTest extends \PHPUnit\Framework\TestCase { self::assertNull($this->response->get('asdf')); self::assertEquals('', $this->response->getBody()); + self::assertTrue(ISO639x1Enum::isValidValue($this->response->getLanguage())); } /** diff --git a/tests/Stdlib/Graph/EdgeTest.php b/tests/Stdlib/Graph/EdgeTest.php index 06ab8c5fc..a718d022c 100644 --- a/tests/Stdlib/Graph/EdgeTest.php +++ b/tests/Stdlib/Graph/EdgeTest.php @@ -35,7 +35,7 @@ class EdgeTest extends \PHPUnit\Framework\TestCase self::assertEquals([new Node('1'), new Node('2')], $edge->getNodes()); self::assertTrue($edge->getNode1()->isEqual(new Node('1'))); self::assertTrue($edge->getNode2()->isEqual(new Node('2'))); - self::assertEquals(0.0, $edge->getWeight()); + self::assertEquals(1.0, $edge->getWeight()); self::assertFalse($edge->isDirected()); } @@ -57,8 +57,8 @@ class EdgeTest extends \PHPUnit\Framework\TestCase */ public function testWeightInputOutput() : void { - $edge = new Edge(new Node('7'), new Node('8'), 1.0, true); - self::assertEquals(1.0, $edge->getWeight()); + $edge = new Edge(new Node('7'), new Node('8'), 2.0, true); + self::assertEquals(2.0, $edge->getWeight()); $edge = new Edge(new Node('7'), new Node('8'), 1.0); $edge->setWeight(3.0); diff --git a/tests/Stdlib/Graph/GraphTest.php b/tests/Stdlib/Graph/GraphTest.php index a82c555b6..7cda956ef 100644 --- a/tests/Stdlib/Graph/GraphTest.php +++ b/tests/Stdlib/Graph/GraphTest.php @@ -58,11 +58,8 @@ class GraphTest extends \PHPUnit\Framework\TestCase self::assertEquals([], $this->graph->getBridges()); self::assertEquals([], $this->graph->getFloydWarshallShortestPath()); self::assertEquals([], $this->graph->getDijkstraShortestPath()); - self::assertEquals([], $this->graph->depthFirstTraversal()); - self::assertEquals([], $this->graph->breadthFirstTraversal()); self::assertEquals([], $this->graph->longestPath()); self::assertEquals([], $this->graph->longestPathBetweenNodes('invalid1', 'invalid2')); - self::assertEquals([], $this->graph->getUnconnected()); self::assertEquals(0, $this->graph->getCost()); self::assertEquals($this->graph, $this->graph->getKruskalMinimalSpanningTree()); diff --git a/tests/System/File/FileUtilsTest.php b/tests/System/File/FileUtilsTest.php index 53c61c54b..02903e1cc 100644 --- a/tests/System/File/FileUtilsTest.php +++ b/tests/System/File/FileUtilsTest.php @@ -101,6 +101,7 @@ class FileUtilsTest extends \PHPUnit\Framework\TestCase self::assertEquals(\basename(__DIR__ . '/FileUtilsTest.php'), FileUtils::mb_pathinfo(__DIR__ . '/FileUtilsTest.php', \PATHINFO_BASENAME)); self::assertEquals('php', FileUtils::mb_pathinfo(__DIR__ . '/FileUtilsTest.php', \PATHINFO_EXTENSION)); self::assertEquals('FileUtilsTest', FileUtils::mb_pathinfo(__DIR__ . '/FileUtilsTest.php', \PATHINFO_FILENAME)); + self::assertEquals( [ 'dirname' => __DIR__, @@ -108,7 +109,7 @@ class FileUtilsTest extends \PHPUnit\Framework\TestCase 'extension' => 'php', 'filename' => 'FileUtilsTest', ], - FileUtils::mb_pathinfo(__DIR__ . '/FileUtilsTest.php', \PATHINFO_FILENAME) + FileUtils::mb_pathinfo(__DIR__ . '/FileUtilsTest.php') ); } }