From 8e8debbf98e7853e84783515296112da166cc0f9 Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Fri, 2 Oct 2020 11:39:44 +0200 Subject: [PATCH 1/5] Progress on `Graph` traversal API --- packages/alfa-graph/package.json | 2 +- packages/alfa-graph/src/graph.ts | 89 +++++++++++++++++++++++--- packages/alfa-graph/test/graph.spec.ts | 15 +++++ packages/alfa-graph/tsconfig.json | 3 - 4 files changed, 95 insertions(+), 14 deletions(-) diff --git a/packages/alfa-graph/package.json b/packages/alfa-graph/package.json index 00ff284094..c5c3144b31 100644 --- a/packages/alfa-graph/package.json +++ b/packages/alfa-graph/package.json @@ -22,7 +22,7 @@ "@siteimprove/alfa-iterable": "^0.5.0", "@siteimprove/alfa-json": "^0.5.0", "@siteimprove/alfa-map": "^0.5.0", - "@siteimprove/alfa-option": "^0.5.0", + "@siteimprove/alfa-sequence": "^0.5.0", "@siteimprove/alfa-set": "^0.5.0" }, "devDependencies": { diff --git a/packages/alfa-graph/src/graph.ts b/packages/alfa-graph/src/graph.ts index 4cdd8c101f..5ce784bd84 100644 --- a/packages/alfa-graph/src/graph.ts +++ b/packages/alfa-graph/src/graph.ts @@ -2,7 +2,7 @@ import { Equatable } from "@siteimprove/alfa-equatable"; import { Iterable } from "@siteimprove/alfa-iterable"; import { Serializable } from "@siteimprove/alfa-json"; import { Map } from "@siteimprove/alfa-map"; -import { Option } from "@siteimprove/alfa-option"; +import { Sequence } from "@siteimprove/alfa-sequence"; import { Set } from "@siteimprove/alfa-set"; import * as json from "@siteimprove/alfa-json"; @@ -33,8 +33,8 @@ export class Graph return this._nodes.keys(); } - public neighbors(node: T): Option> { - return this._nodes.get(node); + public neighbors(node: T): Iterable { + return this._nodes.get(node).getOr([]); } public has(node: T): boolean { @@ -42,13 +42,11 @@ export class Graph } public add(node: T): Graph { - const nodes = this._nodes; - - if (nodes.has(node)) { + if (this.has(node)) { return this; } - return new Graph(nodes.set(node, Set.empty())); + return new Graph(this._nodes.set(node, Set.empty())); } public delete(node: T): Graph { @@ -86,12 +84,12 @@ export class Graph } public disconnect(from: T, to: T): Graph { - const nodes = this._nodes; - - if (!nodes.has(from) || !nodes.has(to)) { + if (!this.has(from) || !this.has(to)) { return this; } + const nodes = this._nodes; + return new Graph( nodes.set( from, @@ -103,6 +101,21 @@ export class Graph ); } + public traverse( + root: T, + traversal: Graph.Traversal = Graph.DepthFirst + ): Sequence { + return Sequence.from(traversal(this, root)); + } + + public hasPath(from: T, to: T): boolean { + if (!this.has(from) || !this.has(to)) { + return false; + } + + return this.traverse(from).includes(to); + } + public equals(value: unknown): value is this { return value instanceof Graph && value._nodes.equals(this._nodes); } @@ -158,4 +171,60 @@ export namespace Graph { ) ); } + + export interface Traversal { + (graph: Graph, root: T): Iterable; + } + + /** + * @see https://en.wikipedia.org/wiki/Depth-first_search + */ + export const DepthFirst: Traversal = function* (graph: Graph, root: T) { + const queue = [root]; + + let seen = Set.empty(); + + while (queue.length > 0) { + const next = queue.pop()!; + + if (seen.has(next)) { + continue; + } + + yield next; + + seen = seen.add(next); + + for (const neighbor of graph.neighbors(next)) { + queue.push(neighbor); + } + } + }; + + /** + * @see https://en.wikipedia.org/wiki/Breadth-first_search + */ + export const BreadthFirst: Traversal = function* ( + graph: Graph, + root: T + ) { + const queue = [root]; + + let seen = Set.of(root); + + while (queue.length > 0) { + const next = queue.shift()!; + + yield next; + + for (const neighbor of graph.neighbors(next)) { + if (seen.has(neighbor)) { + continue; + } + + seen = seen.add(neighbor); + queue.push(neighbor); + } + } + }; } diff --git a/packages/alfa-graph/test/graph.spec.ts b/packages/alfa-graph/test/graph.spec.ts index c6366223c1..2a8e4426a8 100644 --- a/packages/alfa-graph/test/graph.spec.ts +++ b/packages/alfa-graph/test/graph.spec.ts @@ -30,3 +30,18 @@ test("#delete() removes a node from a graph", (t) => { ["foo", ["baz"]], ]); }); + +test("#traverse() traverses the subgraph rooted at a node", (t) => { + t.deepEqual([...graph.traverse("baz")], ["baz", "foo", "bar"]); +}); + +test("#hasPath() checks if there's a path from one node to another", (t) => { + // foo -> bar + t.equal(graph.hasPath("foo", "bar"), true); + + // baz -> foo -> bar + t.equal(graph.hasPath("baz", "bar"), true); + + // bar -> + t.equal(graph.hasPath("bar", "baz"), false); +}); diff --git a/packages/alfa-graph/tsconfig.json b/packages/alfa-graph/tsconfig.json index b168cf092c..c6ff89e1bb 100644 --- a/packages/alfa-graph/tsconfig.json +++ b/packages/alfa-graph/tsconfig.json @@ -15,9 +15,6 @@ { "path": "../alfa-map" }, - { - "path": "../alfa-option" - }, { "path": "../alfa-set" }, From d84b3aad3cfb8f44b1322df42d0981a4d996b65d Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Mon, 5 Oct 2020 10:30:20 +0100 Subject: [PATCH 2/5] Add missing reference --- packages/alfa-graph/tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/alfa-graph/tsconfig.json b/packages/alfa-graph/tsconfig.json index c6ff89e1bb..8cb78cb5ef 100644 --- a/packages/alfa-graph/tsconfig.json +++ b/packages/alfa-graph/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../alfa-map" }, + { + "path": "../alfa-sequence" + }, { "path": "../alfa-set" }, From 0ad8e416f0cf90767d0a5a5eced8fc6cf2b5d7d9 Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Mon, 5 Oct 2020 11:21:26 +0100 Subject: [PATCH 3/5] Add more test cases --- packages/alfa-graph/test/graph.spec.ts | 98 +++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/packages/alfa-graph/test/graph.spec.ts b/packages/alfa-graph/test/graph.spec.ts index 2a8e4426a8..218cbbea3c 100644 --- a/packages/alfa-graph/test/graph.spec.ts +++ b/packages/alfa-graph/test/graph.spec.ts @@ -2,6 +2,11 @@ import { test } from "@siteimprove/alfa-test"; import { Graph } from "../src/graph"; +// foo +// |- bar +// |- baz +// |- foo +// |- ... const graph = Graph.from([ ["foo", ["bar", "baz"]], ["bar", []], @@ -9,6 +14,13 @@ const graph = Graph.from([ ]); test("#connect() connects two nodes in a graph", (t) => { + // foo + // |- bar + // |- baz + // |- ... + // |- baz + // |- foo + // |- ... t.deepEqual(graph.connect("bar", "baz").toArray(), [ ["baz", ["foo"]], ["foo", ["baz", "bar"]], @@ -17,6 +29,11 @@ test("#connect() connects two nodes in a graph", (t) => { }); test("#disconnect() disconnects two nodes in a graph", (t) => { + // foo + // |- baz + // |- foo + // |- ... + // bar t.deepEqual(graph.disconnect("foo", "bar").toArray(), [ ["baz", ["foo"]], ["foo", ["baz"]], @@ -25,6 +42,10 @@ test("#disconnect() disconnects two nodes in a graph", (t) => { }); test("#delete() removes a node from a graph", (t) => { + // foo + // |- baz + // |- foo + // |- ... t.deepEqual(graph.delete("bar").toArray(), [ ["baz", ["foo"]], ["foo", ["baz"]], @@ -32,16 +53,89 @@ test("#delete() removes a node from a graph", (t) => { }); test("#traverse() traverses the subgraph rooted at a node", (t) => { - t.deepEqual([...graph.traverse("baz")], ["baz", "foo", "bar"]); + t.deepEqual([...graph.traverse("baz")].sort(), ["baz", "foo", "bar"].sort()); +}); + +test("#traverse() traverses the subgraph rooted at a node depth-first", (t) => { + // 1 + // |- 2 + // |- 3 + // |- 4 + // |- 5 + // |- 6 + // |- 7 + const graph = Graph.from([ + [1, [2, 5]], + [2, [3, 4]], + [5, [6, 7]], + ]); + + t.deepEqual( + [...graph.traverse(1, Graph.DepthFirst)], + [ + // 1 + 1, + // |- 5 + 5, + // |- 7 + 7, + // |- 6 + 6, + // |- 2 + 2, + // |- 3 + 3, + // |- 4 + 4, + ] + ); +}); + +test("#traverse() traverses the subgraph rooted at a node breadth-first", (t) => { + // 1 + // |- 2 + // |- 3 + // |- 4 + // |- 5 + // |- 6 + // |- 7 + const graph = Graph.from([ + [1, [2, 5]], + [2, [3, 4]], + [5, [6, 7]], + ]); + + t.deepEqual( + [...graph.traverse(1, Graph.BreadthFirst)], + [ + // 1 + 1, + // |- 2 + 2, + // |- 5 + 5, + // |- 4 + 4, + // |- 3 + 3, + // |- 6 + 6, + // |- 7 + 7, + ] + ); }); test("#hasPath() checks if there's a path from one node to another", (t) => { + // foo always has a path to itself + t.equal(graph.hasPath("foo", "foo"), true); + // foo -> bar t.equal(graph.hasPath("foo", "bar"), true); // baz -> foo -> bar t.equal(graph.hasPath("baz", "bar"), true); - // bar -> + // bar has no neighbors t.equal(graph.hasPath("bar", "baz"), false); }); From 3232289e42506a2b7ba3f79dc0ff98027ca21feb Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Mon, 5 Oct 2020 11:24:16 +0100 Subject: [PATCH 4/5] Adjust comments --- packages/alfa-graph/test/graph.spec.ts | 42 +++++++++----------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/packages/alfa-graph/test/graph.spec.ts b/packages/alfa-graph/test/graph.spec.ts index 218cbbea3c..499ca5afb1 100644 --- a/packages/alfa-graph/test/graph.spec.ts +++ b/packages/alfa-graph/test/graph.spec.ts @@ -73,20 +73,13 @@ test("#traverse() traverses the subgraph rooted at a node depth-first", (t) => { t.deepEqual( [...graph.traverse(1, Graph.DepthFirst)], [ - // 1 - 1, - // |- 5 - 5, - // |- 7 - 7, - // |- 6 - 6, - // |- 2 - 2, - // |- 3 - 3, - // |- 4 - 4, + 1, // 1 + 5, // |- 5 + 7, // |- 7 + 6, // |- 6 + 2, // |- 2 + 3, // |- 3 + 4, // |- 4 ] ); }); @@ -108,20 +101,13 @@ test("#traverse() traverses the subgraph rooted at a node breadth-first", (t) => t.deepEqual( [...graph.traverse(1, Graph.BreadthFirst)], [ - // 1 - 1, - // |- 2 - 2, - // |- 5 - 5, - // |- 4 - 4, - // |- 3 - 3, - // |- 6 - 6, - // |- 7 - 7, + 1, // 1 + 2, // |- 2 + 5, // |- 5 + 4, // |- 4 + 3, // |- 3 + 6, // |- 6 + 7, // |- 7 ] ); }); From e7585cf6addcca6e03a709e9d97c873daaa80494 Mon Sep 17 00:00:00 2001 From: Kasper Isager Date: Tue, 6 Oct 2020 09:58:01 +0100 Subject: [PATCH 5/5] Tweaks --- packages/alfa-graph/src/graph.ts | 8 ++++---- packages/alfa-graph/test/graph.spec.ts | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/alfa-graph/src/graph.ts b/packages/alfa-graph/src/graph.ts index 5ce784bd84..366a343e27 100644 --- a/packages/alfa-graph/src/graph.ts +++ b/packages/alfa-graph/src/graph.ts @@ -180,12 +180,12 @@ export namespace Graph { * @see https://en.wikipedia.org/wiki/Depth-first_search */ export const DepthFirst: Traversal = function* (graph: Graph, root: T) { - const queue = [root]; + const stack = [root]; let seen = Set.empty(); - while (queue.length > 0) { - const next = queue.pop()!; + while (stack.length > 0) { + const next = stack.pop()!; if (seen.has(next)) { continue; @@ -196,7 +196,7 @@ export namespace Graph { seen = seen.add(next); for (const neighbor of graph.neighbors(next)) { - queue.push(neighbor); + stack.push(neighbor); } } }; diff --git a/packages/alfa-graph/test/graph.spec.ts b/packages/alfa-graph/test/graph.spec.ts index 499ca5afb1..48fe0b7308 100644 --- a/packages/alfa-graph/test/graph.spec.ts +++ b/packages/alfa-graph/test/graph.spec.ts @@ -113,9 +113,6 @@ test("#traverse() traverses the subgraph rooted at a node breadth-first", (t) => }); test("#hasPath() checks if there's a path from one node to another", (t) => { - // foo always has a path to itself - t.equal(graph.hasPath("foo", "foo"), true); - // foo -> bar t.equal(graph.hasPath("foo", "bar"), true); @@ -124,4 +121,9 @@ test("#hasPath() checks if there's a path from one node to another", (t) => { // bar has no neighbors t.equal(graph.hasPath("bar", "baz"), false); + + // A node always has a path to itself + for (const node of ["foo", "bar", "baz"]) { + t.equal(graph.hasPath(node, node), true); + } });