From b208b7f7fdadb4cb93abf6000a81bcac556eda48 Mon Sep 17 00:00:00 2001 From: Haydn Ewers <27211+haydn@users.noreply.github.com> Date: Fri, 26 Jan 2024 18:00:18 +1100 Subject: [PATCH] improve types --- .gitignore | 10 +- index.ts | 217 +++++--- package.json | 2 +- test.ts | 1137 -------------------------------------- tests/addEdge.ts | 52 ++ tests/addVertex.ts | 41 ++ tests/ancestors.ts | 41 ++ tests/children.ts | 29 + tests/clone.ts | 21 + tests/create.ts | 15 + tests/degree.ts | 83 +++ tests/descendants.ts | 41 ++ tests/edges.ts | 58 ++ tests/fromD3.ts | 41 ++ tests/getEdge.ts | 17 + tests/indegree.ts | 31 ++ tests/isCyclic.ts | 128 +++++ tests/isUndirected.ts | 26 + tests/makeUndirected.ts | 52 ++ tests/order.ts | 23 + tests/outdegree.ts | 31 ++ tests/parents.ts | 28 + tests/removeEdge.ts | 52 ++ tests/removeVertex.ts | 21 + tests/setEdge.ts | 68 +++ tests/size.ts | 48 ++ tests/toD3.ts | 51 ++ tests/toDirected.ts | 6 + tests/topologicalSort.ts | 55 ++ tests/transpose.ts | 63 +++ tests/vertexPairs.ts | 22 + tests/vertices.ts | 15 + tsconfig.json | 19 + 33 files changed, 1315 insertions(+), 1229 deletions(-) delete mode 100644 test.ts create mode 100644 tests/addEdge.ts create mode 100644 tests/addVertex.ts create mode 100644 tests/ancestors.ts create mode 100644 tests/children.ts create mode 100644 tests/clone.ts create mode 100644 tests/create.ts create mode 100644 tests/degree.ts create mode 100644 tests/descendants.ts create mode 100644 tests/edges.ts create mode 100644 tests/fromD3.ts create mode 100644 tests/getEdge.ts create mode 100644 tests/indegree.ts create mode 100644 tests/isCyclic.ts create mode 100644 tests/isUndirected.ts create mode 100644 tests/makeUndirected.ts create mode 100644 tests/order.ts create mode 100644 tests/outdegree.ts create mode 100644 tests/parents.ts create mode 100644 tests/removeEdge.ts create mode 100644 tests/removeVertex.ts create mode 100644 tests/setEdge.ts create mode 100644 tests/size.ts create mode 100644 tests/toD3.ts create mode 100644 tests/toDirected.ts create mode 100644 tests/topologicalSort.ts create mode 100644 tests/transpose.ts create mode 100644 tests/vertexPairs.ts create mode 100644 tests/vertices.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 8e73f02..744c988 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ /.vscode +/**/*.d.ts +/**/*.js +/**/*.js.flow /cjs /esm -/index.d.ts -/index.js -/index.js.flow /node_modules -/test.js -/umd +/tsconfig.tsbuildinfo +/umd \ No newline at end of file diff --git a/index.ts b/index.ts index 9e0becc..e39e861 100644 --- a/index.ts +++ b/index.ts @@ -1,23 +1,21 @@ -type D3Graph = { +type Graph = Record>; + +type D3Graph = { nodes: Array<{ - id: string; + id: T; }>; links: Array<{ - source: string; - target: string; + source: T; + target: T; }>; }; -type Edge = [string, string]; - -type Graph = { - [u: string]: { - [v: string]: number; - }; -}; - -const addEdge = (graph: Graph, [u, v]: Edge, options?: { undirected?: boolean }): Graph => { - const result = clone(graph); +const addEdge = ( + graph: Graph, + [u, v]: [T, T], + options?: { undirected?: boolean }, +): Graph => { + const result = clone(graph); if (result[u][v] === 0) { result[u][v] = 1; @@ -30,22 +28,28 @@ const addEdge = (graph: Graph, [u, v]: Edge, options?: { undirected?: boolean }) return result; }; -const addVertex = (graph: Graph, vertex: string): Graph => { - if (graph[vertex]) return graph; +const addVertex = ( + graph: Graph, + vertex: T2, +): Graph => { + if (vertex in graph) return graph as Graph; - const result = clone(graph); + const result = clone(graph) as Graph; for (let v in result) result[v][vertex] = 0; - result[vertex] = {}; + result[vertex] = {} as Graph[T2]; for (let v in result) result[vertex][v] = 0; return result; }; -const ancestors = (graph: Graph, vertex: string): Set => { +const ancestors = ( + graph: Graph, + vertex: T2, +): Set => { if (isCyclic(graph)) throw "Cannot retrieve ancestors in a graph that contains cycles."; - let result: Set = new Set(); + let result: Set = new Set(); for (let parent of parents(graph, vertex)) { result = new Set([...result, parent, ...ancestors(graph, parent)]); @@ -54,10 +58,14 @@ const ancestors = (graph: Graph, vertex: string): Set => { return result; }; -const children = (graph: Graph, vertex: string): Set => { - const result: Set = new Set(); +const children = ( + graph: Graph, + vertex: T2, +): Set => { + const result: Set = new Set(); + const vertices = Object.keys(graph) as Array; - for (let v in graph[vertex]) { + for (let v of vertices) { if (graph[vertex][v] !== 0) { result.add(v); } @@ -66,11 +74,11 @@ const children = (graph: Graph, vertex: string): Set => { return result; }; -const clone = (graph: Graph): Graph => { - const result: Graph = {}; +const clone = (graph: Graph): Graph => { + const result = {} as Graph; for (let u in graph) { - result[u] = {}; + result[u] = {} as Graph[T]; for (let v in graph[u]) { result[u][v] = graph[u][v]; } @@ -79,14 +87,14 @@ const clone = (graph: Graph): Graph => { return result; }; -const create = (size: number = 0, id: (i: number) => string = (i) => i.toString(10)): Graph => { - const result: Graph = {}; +const create = (size: number, identFn: (i: number) => T): Graph => { + const result = {} as Graph; for (let i = 0; i < size; i++) { - const u = id(i); - result[u] = {}; + const u = identFn(i); + result[u] = {} as Graph[T]; for (let j = 0; j < size; j++) { - const v = id(j); + const v = identFn(j); result[u][v] = 0; } } @@ -94,9 +102,9 @@ const create = (size: number = 0, id: (i: number) => string = (i) => i.toString( return result; }; -const degree = ( - graph: Graph, - vertex: string, +const degree = ( + graph: Graph, + vertex: T2, options?: { weighted?: boolean; undirected?: boolean }, ): number => { if (options?.undirected && !isUndirected(graph)) { @@ -111,12 +119,15 @@ const degree = ( ); }; -const descendants = (graph: Graph, vertex: string): Set => { +const descendants = ( + graph: Graph, + vertex: T2, +): Set => { if (isCyclic(graph)) { throw Error("Cannot retrieve descendants in a graph that contains cycles."); } - let result: Set = new Set(); + let result: Set = new Set(); for (let child of children(graph, vertex)) { result = new Set([...result, child, ...descendants(graph, child)]); @@ -125,16 +136,20 @@ const descendants = (graph: Graph, vertex: string): Set => { return result; }; -const edges = (graph: Graph, options?: { undirected?: boolean }): Set => { +const edges = ( + graph: Graph, + options?: { undirected?: boolean }, +): Set<[T, T]> => { if (options?.undirected && !isUndirected(graph)) { throw Error("Expected undirected graph, but got a directed graph."); } const resolvedGraph = options?.undirected ? toDirected(graph) : graph; - const result: Set = new Set([]); + const result: Set<[T, T]> = new Set([]); + const vertices = Object.keys(resolvedGraph) as Array; - for (let u in resolvedGraph) { - for (let v in resolvedGraph[u]) { + for (let u of vertices) { + for (let v of vertices) { if (resolvedGraph[u][v] !== 0) { result.add([u, v]); } @@ -144,11 +159,14 @@ const edges = (graph: Graph, options?: { undirected?: boolean }): Set => { return result; }; -const fromD3 = (d3Graph: D3Graph, options?: { undirected?: boolean }): Graph => { - const result: Graph = {}; +const fromD3 = ( + d3Graph: D3Graph, + options?: { undirected?: boolean }, +): Graph => { + const result = {} as Graph; for (let u of d3Graph.nodes) { - result[u.id] = {}; + result[u.id] = {} as Graph[T]; for (let v of d3Graph.nodes) { result[u.id][v.id] = 0; } @@ -162,9 +180,13 @@ const fromD3 = (d3Graph: D3Graph, options?: { undirected?: boolean }): Graph => return result; }; -const getEdge = (graph: Graph, [u, v]: Edge): number => graph[u][v]; +const getEdge = (graph: Graph, [u, v]: [T, T]): number => graph[u][v]; -const indegree = (graph: Graph, vertex: string, options?: { weighted?: boolean }): number => { +const indegree = ( + graph: Graph, + vertex: T2, + options?: { weighted?: boolean }, +): number => { let result = 0; for (let u in graph) { @@ -243,10 +265,10 @@ const isUndirected = (graph: Graph): boolean => { return true; }; -const makeUndirected = ( - graph: Graph, +const makeUndirected = ( + graph: Graph, merge: (a: number, b: number) => number = (a, b) => Math.max(a, b), -): Graph => { +): Graph => { const result = clone(graph); for (let [u, v] of vertexPairs(graph)) { @@ -267,7 +289,11 @@ const order = (graph: Graph): number => { return result; }; -const outdegree = (graph: Graph, vertex: string, options?: { weighted?: boolean }): number => { +const outdegree = ( + graph: Graph, + vertex: T2, + options?: { weighted?: boolean }, +): number => { let result = 0; for (let v in graph[vertex]) { @@ -279,8 +305,11 @@ const outdegree = (graph: Graph, vertex: string, options?: { weighted?: boolean return result; }; -const parents = (graph: Graph, vertex: string): Set => { - const result: Set = new Set(); +const parents = ( + graph: Graph, + vertex: T2, +): Set => { + const result: Set = new Set(); for (let u in graph) { if (graph[u][vertex] !== 0) { @@ -291,7 +320,11 @@ const parents = (graph: Graph, vertex: string): Set => { return result; }; -const removeEdge = (graph: Graph, [u, v]: Edge, options?: { undirected?: boolean }): Graph => { +const removeEdge = ( + graph: Graph, + [u, v]: [T2, T2], + options?: { undirected?: boolean }, +): Graph => { const result = clone(graph); result[u][v] = 0; @@ -300,14 +333,17 @@ const removeEdge = (graph: Graph, [u, v]: Edge, options?: { undirected?: boolean return result; }; -const removeVertex = (graph: Graph, vertex: string): Graph => { - const result: Graph = {}; - - for (let u in graph) { - if (u === vertex) continue; - result[u] = {}; - for (let v in graph[u]) { - if (v === vertex) continue; +const removeVertex = ( + graph: Graph, + vertex: T2, +): Graph> => { + const result = {} as Graph>; + const vertices = Object.keys(graph) as Array; + const newVertices = vertices.filter((x): x is Exclude => x !== vertex); + + for (let u of newVertices) { + result[u] = {} as Graph>[Exclude]; + for (let v of newVertices) { result[u][v] = graph[u][v]; } } @@ -315,12 +351,12 @@ const removeVertex = (graph: Graph, vertex: string): Graph => { return result; }; -const setEdge = ( - graph: Graph, - [u, v]: Edge, +const setEdge = ( + graph: Graph, + [u, v]: [T, T], weight: number, options?: { undirected?: boolean }, -): Graph => { +): Graph => { const result = clone(graph); result[u][v] = weight; @@ -348,18 +384,22 @@ const size = (graph: Graph, options?: { undirected?: boolean }): number => { return result; }; -const toD3 = (graph: Graph, options?: { undirected?: boolean }): D3Graph => { +const toD3 = ( + graph: Graph, + options?: { undirected?: boolean }, +): D3Graph => { if (options?.undirected && !isUndirected(graph)) { throw Error("Expected undirected graph, but got a directed graph."); } const resolvedGraph = options?.undirected ? toDirected(graph) : graph; - const nodes: Array<{ id: string }> = []; - const links: Array<{ source: string; target: string }> = []; + const nodes: Array<{ id: T }> = []; + const links: Array<{ source: T; target: T }> = []; + const vertices = Object.keys(resolvedGraph) as Array; - for (let u in resolvedGraph) { + for (let u of vertices) { nodes[nodes.length] = { id: u }; - for (let v in resolvedGraph[u]) { + for (let v of vertices) { if (resolvedGraph[u][v] !== 0) { let i = resolvedGraph[u][v]; do { @@ -373,18 +413,20 @@ const toD3 = (graph: Graph, options?: { undirected?: boolean }): D3Graph => { return { nodes, links }; }; -const toDirected = (graph: Graph): Graph => { +const toDirected = (graph: Graph): Graph => { if (!isUndirected(graph)) { return graph; } - const result: Graph = {}; - const vertices = Object.keys(graph); + const result = {} as Graph; + const vertices = Object.keys(graph) as Array; for (let u = 0; u < vertices.length; u++) { - result[vertices[u]] = {}; + result[vertices[u]] = {} as Graph[T]; for (let v = 0; v < vertices.length; v++) { - result[vertices[u]][vertices[v]] = v >= u ? graph[vertices[u]][vertices[v]] : 0; + const uuu = vertices[u]; + const vvv = vertices[v]; + result[uuu][vvv] = v >= u ? graph[vertices[u]][vertices[v]] : 0; } } @@ -398,8 +440,9 @@ const topologicalSort = (graph: Graph): Array => { const visited: Set = new Set(); const queue: Array = []; const indegrees: { [id: string]: number } = {}; + const vertices = Object.keys(graph); - for (let v of vertices(graph)) { + for (let v of vertices) { indegrees[v] = indegree(graph, v); } @@ -427,12 +470,13 @@ const topologicalSort = (graph: Graph): Array => { return result; }; -const transpose = (graph: Graph): Graph => { - const result: Graph = {}; +const transpose = (graph: Graph): Graph => { + const result = {} as Graph; + const vertices = Object.keys(graph) as Array; - for (let u in graph) { - result[u] = {}; - for (let v in graph[u]) { + for (let u of vertices) { + result[u] = {} as Graph[T]; + for (let v of vertices) { result[u][v] = graph[v][u]; } } @@ -440,11 +484,12 @@ const transpose = (graph: Graph): Graph => { return result; }; -const vertices = (graph: Graph): Set => new Set(Object.keys(graph)); +const vertices = (graph: Graph): Set => + new Set(Object.keys(graph) as Array); -const vertexPairs = (graph: Graph): Set<[string, string]> => { - const result: Set<[string, string]> = new Set(); - const vertices = Object.keys(graph); +const vertexPairs = (graph: Graph): Set<[T, T]> => { + const result: Set<[T, T]> = new Set(); + const vertices = Object.keys(graph) as Array; for (let u = 0; u < vertices.length; u++) { for (let v = u; v < vertices.length; v++) { @@ -456,8 +501,6 @@ const vertexPairs = (graph: Graph): Set<[string, string]> => { }; export { - D3Graph, - Graph, addEdge, addVertex, ancestors, @@ -488,3 +531,5 @@ export { vertices, vertexPairs, }; + +export type { D3Graph, Graph }; diff --git a/package.json b/package.json index a1f48bc..5fe281d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "build:umd": "tsc index.ts --downlevelIteration --target ES2015 --module UMD --outDir umd", "build:esm": "tsc index.ts --target ESNext --module ESNext --outDir esm", "release": "np", - "test": "prettier -c index.ts README.md test.ts && tsc test.ts --downlevelIteration --lib ES2015 --esModuleInterop && node test.js", + "test": "prettier -c **/*.ts **/*.md && tsc && tape tests/**/*.js", "version": "yarn build" }, "files": [ diff --git a/test.ts b/test.ts deleted file mode 100644 index de04b7c..0000000 --- a/test.ts +++ /dev/null @@ -1,1137 +0,0 @@ -import { - addEdge, - addVertex, - ancestors, - children, - clone, - create, - degree, - descendants, - edges, - fromD3, - getEdge, - indegree, - isCyclic, - isUndirected, - makeUndirected, - order, - outdegree, - parents, - removeEdge, - removeVertex, - setEdge, - size, - toD3, - topologicalSort, - transpose, - vertices, - vertexPairs, -} from "./index"; - -import test from "tape"; - -test("addEdge", (t) => { - t.plan(3); - - t.deepEqual( - addEdge( - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - ), - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - "Base case", - ); - - t.deepEqual( - addEdge( - { - a: { a: 0, b: 1.5 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - ), - { - a: { a: 0, b: 1.5 }, - b: { a: 0, b: 0 }, - }, - "Adding an edge that already exists should be a no-op", - ); - - t.deepEqual( - addEdge( - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - { undirected: true }, - ), - { - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }, - "Add undirected edge", - ); -}); - -test("addVertex", (t) => { - t.plan(3); - - t.deepEqual( - addVertex({}, "a"), - { - a: { a: 0 }, - }, - "Base case", - ); - - t.deepEqual( - addVertex( - { - a: { a: 0 }, - }, - "b", - ), - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - "Adding a vertex to an existing graph should create empty rows and columns", - ); - - t.deepEqual( - addVertex( - { - a: { a: 0 }, - }, - "a", - ), - { - a: { a: 0 }, - }, - "Adding a vertex that already exists should be a no-op", - ); -}); - -test("ancestors", (t) => { - t.plan(3); - - t.deepEqual( - ancestors( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }, - "c", - ), - new Set(["a", "b"]), - ); - - t.deepEqual( - ancestors( - { - a: { a: 0, b: 1, c: 0, d: 0 }, - b: { a: 0, b: 0, c: 1, d: 1 }, - c: { a: 0, b: 0, c: 0, d: 1 }, - d: { a: 0, b: 0, c: 0, d: 0 }, - }, - "d", - ), - new Set(["a", "b", "c"]), - ); - - t.throws(() => { - ancestors( - { - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }, - "b", - ); - }, "Graphs with cycles should throw an error"); -}); - -test("children", (t) => { - t.plan(2); - - t.deepEqual( - children( - { - a: { a: 0, b: 1, c: 1 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }, - "a", - ), - new Set(["b", "c"]), - ); - - t.deepEqual( - children( - { - a: { a: 1 }, - }, - "a", - ), - new Set(["a"]), - "Loops should cause the vertex to be listed as a child of itself", - ); -}); - -test("clone", (t) => { - t.plan(4); - - const original = { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }; - - t.deepEqual( - clone(original), - original, - "Cloning should return a graph exactly equal to the original graph", - ); - - t.notEqual(clone(original), original, "Cloning should make a copy of the graph"); - t.notEqual(clone(original).a, original.a, "Cloning should make a copy of the graph"); - t.notEqual(clone(original).b, original.b, "Cloning should make a copy of the graph"); -}); - -test("create", (t) => { - t.plan(2); - - t.deepEqual(create(1), { 0: { 0: 0 } }), "Base case"; - - t.deepEqual( - create(2, (i) => `${i}!`), - { - "0!": { "0!": 0, "1!": 0 }, - "1!": { "0!": 0, "1!": 0 }, - }, - "Custom ID function is used", - ); -}); - -test("degree", (t) => { - t.plan(7); - - t.equal(degree({ a: { a: 0 } }, "a"), 0, "Base case"); - - t.equal( - degree( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 2, b: 0, c: 0 }, - c: { a: -0.5, b: 0, c: 0 }, - }, - "a", - ), - 3, - "The unweighted degree should be the count of edges", - ); - - t.equal( - degree( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 2, b: 0, c: 0 }, - c: { a: -0.5, b: 0, c: 0 }, - }, - "a", - { weighted: true }, - ), - 2.5, - "The weighted degree should be the sum of edge weights", - ); - - t.equal( - degree( - { - a: { a: 1.5 }, - }, - "a", - ), - 2, - "Loops should count twice towards the degree", - ); - - t.equal( - degree( - { - a: { a: 1.5 }, - }, - "a", - { weighted: true }, - ), - 3, - "Loops should count twice towards the weighted degree", - ); - - t.equal( - degree( - { - a: { a: 0, b: 1, c: 1 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }, - "a", - { undirected: true }, - ), - 2, - "Reciprocal edges should only be counted once in undirected mode", - ); - - t.throws(() => { - degree( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - "a", - { undirected: true }, - ); - }, "Using the undirected option on a directed graph should throw an error"); -}); - -test("descendants", (t) => { - t.plan(3); - - t.deepEqual( - descendants( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }, - "a", - ), - new Set(["b", "c"]), - ); - - t.deepEqual( - descendants( - { - a: { a: 0, b: 1, c: 0, d: 0 }, - b: { a: 0, b: 0, c: 1, d: 1 }, - c: { a: 0, b: 0, c: 0, d: 1 }, - d: { a: 0, b: 0, c: 0, d: 0 }, - }, - "a", - ), - new Set(["b", "c", "d"]), - ); - - t.throws(() => { - descendants( - { - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }, - "a", - ); - }, "A graph with cycles should throw an error"); -}); - -test("edges", (t) => { - t.plan(4); - - t.deepEqual( - edges({ - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }), - new Set([ - ["a", "b"], - ["b", "c"], - ]), - ); - - t.deepEqual( - edges({ - a: { a: 0, b: 2, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0.5, b: 0, c: 0 }, - }), - new Set([ - ["a", "b"], - ["b", "c"], - ["c", "a"], - ]), - ); - - t.deepEqual( - edges( - { - a: { a: 1, b: 1, c: 0 }, - b: { a: 1, b: 0, c: 1 }, - c: { a: 0, b: 1, c: 0 }, - }, - { undirected: true }, - ), - new Set([ - ["a", "a"], - ["a", "b"], - ["b", "c"], - ]), - "Undirected mode", - ); - - t.throws(() => { - edges( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - { undirected: true }, - ); - }, "Using the undirected option on a directed graph should throw an error"); -}); - -test("fromD3", (t) => { - t.plan(2); - - t.deepEqual( - fromD3({ - nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], - links: [ - { source: "a", target: "b" }, - { source: "a", target: "c" }, - { source: "a", target: "a" }, - ], - }), - { - a: { a: 1, b: 1, c: 1 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }, - ); - - t.deepEqual( - fromD3( - { - nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], - links: [ - { source: "a", target: "a" }, - { source: "a", target: "b" }, - { source: "a", target: "c" }, - ], - }, - { undirected: true }, - ), - { - a: { a: 1, b: 1, c: 1 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }, - ); -}); - -test("getEdge", (t) => { - t.plan(1); - - t.equal( - getEdge( - { - a: { a: 0, b: 1.5 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - ), - 1.5, - ); -}); - -test("indegree", (t) => { - t.plan(2); - - t.equal( - indegree( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 2, b: 0, c: 0 }, - c: { a: 0.5, b: 0, c: 0 }, - }, - "a", - ), - 2, - ); - - t.equal( - indegree( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 2, b: 0, c: 0 }, - c: { a: 0.5, b: 0, c: 0 }, - }, - "a", - { weighted: true }, - ), - 2.5, - ); -}); - -test("isCyclic", (t) => { - t.test("directed graph", (t) => { - t.plan(9); - - t.equal(isCyclic({}), false); - - t.equal( - isCyclic({ - a: { a: 0 }, - }), - false, - ); - - t.equal( - isCyclic({ - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }), - false, - ); - - t.equal( - isCyclic({ - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }), - false, - ); - - t.equal( - isCyclic({ - a: { a: 0, b: 1, c: 1 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }), - false, - ); - - t.equal( - isCyclic({ - a: { a: 1 }, - }), - true, - ); - - t.equal( - isCyclic({ - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }), - true, - ); - - t.equal( - isCyclic({ - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 1, b: 0, c: 0 }, - }), - true, - ); - - t.equal( - isCyclic({ - a: { a: 1, b: 0 }, - b: { a: 0, b: 1 }, - }), - true, - ); - }); - - t.test("undirected graph", (t) => { - t.plan(5); - - t.equal(isCyclic({}, { undirected: true }), false, "Base case"); - - t.equal( - isCyclic( - { - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }, - { undirected: true }, - ), - false, - "Single edge", - ); - - t.equal( - isCyclic( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 1, b: 0, c: 1 }, - c: { a: 0, b: 1, c: 0 }, - }, - { undirected: true }, - ), - false, - "2 edge graph", - ); - - t.equal( - isCyclic( - { - a: { a: 0, b: 1, c: 1 }, - b: { a: 1, b: 0, c: 1 }, - c: { a: 1, b: 1, c: 0 }, - }, - { undirected: true }, - ), - true, - "Complete graph", - ); - - t.throws(() => { - isCyclic( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - { undirected: true }, - ); - }, "Using the undirected option on a directed graph should throw an error"); - }); -}); - -test("isUndirected", (t) => { - t.plan(4); - - t.equal(isUndirected({}), true); - - t.equal(isUndirected({ a: { a: 1 } }), true); - - t.equal( - isUndirected({ - a: { a: 1, b: 1 }, - b: { a: 0, b: 0 }, - }), - false, - ); - - t.equal( - isUndirected({ - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }), - true, - ); -}); - -test("makeUndirected", (t) => { - t.plan(5); - - t.deepEqual(makeUndirected({}), {}); - - t.deepEqual(makeUndirected({ a: { a: 1 } }), { a: { a: 1 } }); - - t.deepEqual( - makeUndirected({ - a: { a: 1, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }), - { - a: { a: 1, b: 1, c: 0 }, - b: { a: 1, b: 0, c: 1 }, - c: { a: 0, b: 1, c: 0 }, - }, - ); - - t.deepEqual( - makeUndirected({ - a: { a: 0.5, b: -1, c: 0 }, - b: { a: 0, b: 0, c: -1 }, - c: { a: 0, b: 0, c: 0 }, - }), - { - a: { a: 0.5, b: -1, c: 0 }, - b: { a: -1, b: 0, c: -1 }, - c: { a: 0, b: -1, c: 0 }, - }, - ); - - t.deepEqual( - makeUndirected( - { - a: { a: 2, b: 3, c: 0 }, - b: { a: 2, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }, - (a, b) => a * b, - ), - { - a: { a: 2, b: 6, c: 0 }, - b: { a: 6, b: 0, c: 1 }, - c: { a: 0, b: 1, c: 0 }, - }, - ); -}); - -test("order", (t) => { - t.plan(3); - - t.equal(order({}), 0); - - t.equal( - order({ - a: { a: 0 }, - }), - 1, - ); - - t.equal( - order({ - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }), - 2, - ); -}); - -test("outdegree", (t) => { - t.plan(2); - - t.equal( - outdegree( - { - a: { a: 0, b: 2, c: 0.5 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }, - "a", - ), - 2, - ); - - t.equal( - outdegree( - { - a: { a: 0, b: 2, c: 0.5 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }, - "a", - { weighted: true }, - ), - 2.5, - ); -}); - -test("parents", (t) => { - t.plan(2); - - t.deepEqual( - parents( - { - a: { a: 0, b: 0, c: 1 }, - b: { a: 0, b: 0, c: 0.5 }, - c: { a: 0, b: 0, c: 0 }, - }, - "c", - ), - new Set(["a", "b"]), - ); - - t.deepEqual( - parents( - { - a: { a: 1 }, - }, - "a", - ), - new Set(["a"]), - ); -}); - -test("removeEdge", (t) => { - t.plan(3); - - t.deepEqual( - removeEdge( - { - a: { a: 0, b: 1.5, c: 0 }, - b: { a: 0, b: 0, c: 0.5 }, - c: { a: 2, b: 0, c: 0 }, - }, - ["a", "b"], - ), - { - a: { a: 0, b: 0, c: 0 }, - b: { a: 0, b: 0, c: 0.5 }, - c: { a: 2, b: 0, c: 0 }, - }, - ); - - t.deepEqual( - removeEdge( - { - a: { a: 0, b: 2 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - ), - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - ); - - t.deepEqual( - removeEdge( - { - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }, - ["a", "b"], - { undirected: true }, - ), - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - "Undirected", - ); -}); - -test("removeVertex", (t) => { - t.plan(1); - - t.deepEqual( - removeVertex( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 1, b: 0, c: 0 }, - }, - "b", - ), - { - a: { a: 0, c: 0 }, - c: { a: 1, c: 0 }, - }, - ); -}); - -test("setEdge", (t) => { - t.plan(4); - - t.deepEqual( - setEdge( - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - 1, - ), - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - ); - - t.deepEqual( - setEdge( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - 0, - ), - { - a: { a: 0, b: 0 }, - b: { a: 0, b: 0 }, - }, - ); - - t.deepEqual( - setEdge( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - 1.5, - ), - { - a: { a: 0, b: 1.5 }, - b: { a: 0, b: 0 }, - }, - ); - - t.deepEqual( - setEdge( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - ["a", "b"], - 1.5, - { undirected: true }, - ), - { - a: { a: 0, b: 1.5 }, - b: { a: 1.5, b: 0 }, - }, - "Undirected", - ); -}); - -test("size", (t) => { - t.plan(5); - - t.equal(size({}), 0); - - t.equal( - size({ - a: { a: 1, b: 0 }, - b: { a: 0, b: 0 }, - }), - 1, - ); - - t.equal( - size({ - a: { a: 3, b: 0, c: 0 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }), - 2, - ); - - t.equal( - size( - { - a: { a: 0, b: 1, c: 0 }, - b: { a: 1, b: 0, c: 1 }, - c: { a: 0, b: 1, c: 0 }, - }, - { undirected: true }, - ), - 2, - "Undirected", - ); - - t.throws(() => { - size( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - { undirected: true }, - ); - }, "Using the undirected option on a directed graph should throw an error"); -}); - -test("toD3", (t) => { - t.plan(3); - - t.deepEqual( - toD3({ - a: { a: 1, b: 2, c: 0.5 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }), - { - nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], - links: [ - { source: "a", target: "a" }, - { source: "a", target: "b" }, - { source: "a", target: "b" }, - { source: "a", target: "c" }, - ], - }, - ); - - t.deepEqual( - toD3( - { - a: { a: 0, b: 1, c: 1 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }, - { undirected: true }, - ), - { - nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], - links: [ - { source: "a", target: "b" }, - { source: "a", target: "c" }, - ], - }, - ); - - t.throws(() => { - toD3( - { - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }, - { undirected: true }, - ); - }, "Using the undirected option on a directed graph should throw an error"); -}); - -test("toDirected", (t) => { - t.plan(1); - t.skip("TODO"); -}); - -test("topologicalSort", (t) => { - t.plan(6); - - t.deepEqual( - topologicalSort({ - a: { a: 0 }, - }), - ["a"], - ); - - t.deepEqual( - topologicalSort({ - a: { a: 0, b: 0, c: 0 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }), - ["a", "b", "c"], - ); - - t.deepEqual( - topologicalSort({ - a: { a: 0, b: 1, c: 0 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }), - ["c", "a", "b"], - ); - - t.deepEqual( - topologicalSort({ - a: { a: 0, b: 0, c: 0 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 1, b: 0, c: 0 }, - }), - ["b", "c", "a"], - ); - - t.deepEqual( - topologicalSort({ - a: { a: 0, b: 0 }, - b: { a: 10, b: 0 }, - }), - ["b", "a"], - ); - - t.throws(() => { - topologicalSort({ - a: { a: 0, b: 1 }, - b: { a: 1, b: 0 }, - }); - }); -}); - -test("transpose", (t) => { - t.plan(5); - - t.deepEqual( - transpose({ - a: { a: 0 }, - }), - { - a: { a: 0 }, - }, - ); - - t.deepEqual( - transpose({ - a: { a: 0, b: 1 }, - b: { a: 0, b: 0 }, - }), - { - a: { a: 0, b: 0 }, - b: { a: 1, b: 0 }, - }, - ); - - t.deepEqual( - transpose({ - a: { a: 0, b: -0.5 }, - b: { a: 0, b: 0 }, - }), - { - a: { a: 0, b: 0 }, - b: { a: -0.5, b: 0 }, - }, - ); - - t.deepEqual( - transpose({ - a: { a: 0, b: 1, c: 1 }, - b: { a: 0, b: 0, c: 1 }, - c: { a: 0, b: 0, c: 0 }, - }), - { - a: { a: 0, b: 0, c: 0 }, - b: { a: 1, b: 0, c: 0 }, - c: { a: 1, b: 1, c: 0 }, - }, - ); - - t.deepEqual( - transpose({ - a: { a: 1, b: 1, c: 1 }, - b: { a: 1, b: 1, c: 1 }, - c: { a: 1, b: 1, c: 1 }, - }), - { - a: { a: 1, b: 1, c: 1 }, - b: { a: 1, b: 1, c: 1 }, - c: { a: 1, b: 1, c: 1 }, - }, - ); -}); - -test("vertices", (t) => { - t.plan(1); - - t.deepEqual( - vertices({ - a: { a: 0, b: 0, c: 0 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }), - new Set(["a", "b", "c"]), - ); -}); - -test("vertexPairs", (t) => { - t.plan(1); - - t.deepEqual( - vertexPairs({ - a: { a: 0, b: 0, c: 0 }, - b: { a: 0, b: 0, c: 0 }, - c: { a: 0, b: 0, c: 0 }, - }), - new Set([ - ["a", "a"], - ["a", "b"], - ["a", "c"], - ["b", "b"], - ["b", "c"], - ["c", "c"], - ]), - ); -}); diff --git a/tests/addEdge.ts b/tests/addEdge.ts new file mode 100644 index 0000000..6e883b4 --- /dev/null +++ b/tests/addEdge.ts @@ -0,0 +1,52 @@ +import test from "tape"; +import { addEdge } from ".."; + +test("addEdge", (t) => { + t.plan(3); + + t.deepEqual( + addEdge( + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + ), + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + "Base case", + ); + + t.deepEqual( + addEdge( + { + a: { a: 0, b: 1.5 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + ), + { + a: { a: 0, b: 1.5 }, + b: { a: 0, b: 0 }, + }, + "Adding an edge that already exists should be a no-op", + ); + + t.deepEqual( + addEdge( + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + { undirected: true }, + ), + { + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }, + "Add undirected edge", + ); +}); diff --git a/tests/addVertex.ts b/tests/addVertex.ts new file mode 100644 index 0000000..b7e7f72 --- /dev/null +++ b/tests/addVertex.ts @@ -0,0 +1,41 @@ +import { addVertex } from ".."; +import test from "tape"; + +test("addVertex", (t) => { + t.plan(3); + + t.deepEqual( + addVertex({}, "a"), + { + a: { a: 0 }, + }, + "Base case", + ); + + t.deepEqual( + addVertex( + { + a: { a: 0 }, + }, + "b", + ), + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + "Adding a vertex to an existing graph should create empty rows and columns", + ); + + t.deepEqual( + addVertex( + { + a: { a: 0 }, + }, + "a", + ), + { + a: { a: 0 }, + }, + "Adding a vertex that already exists should be a no-op", + ); +}); diff --git a/tests/ancestors.ts b/tests/ancestors.ts new file mode 100644 index 0000000..7f14fd9 --- /dev/null +++ b/tests/ancestors.ts @@ -0,0 +1,41 @@ +import test from "tape"; +import { ancestors } from ".."; + +test("ancestors", (t) => { + t.plan(3); + + t.deepEqual( + ancestors( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }, + "c", + ), + new Set(["a", "b"]), + ); + + t.deepEqual( + ancestors( + { + a: { a: 0, b: 1, c: 0, d: 0 }, + b: { a: 0, b: 0, c: 1, d: 1 }, + c: { a: 0, b: 0, c: 0, d: 1 }, + d: { a: 0, b: 0, c: 0, d: 0 }, + }, + "d", + ), + new Set(["a", "b", "c"]), + ); + + t.throws(() => { + ancestors( + { + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }, + "b", + ); + }, "Graphs with cycles should throw an error"); +}); diff --git a/tests/children.ts b/tests/children.ts new file mode 100644 index 0000000..b173bbb --- /dev/null +++ b/tests/children.ts @@ -0,0 +1,29 @@ +import test from "tape"; +import { children } from ".."; + +test("children", (t) => { + t.plan(2); + + t.deepEqual( + children( + { + a: { a: 0, b: 1, c: 1 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }, + "a", + ), + new Set(["b", "c"]), + ); + + t.deepEqual( + children( + { + a: { a: 1 }, + }, + "a", + ), + new Set(["a"]), + "Loops should cause the vertex to be listed as a child of itself", + ); +}); diff --git a/tests/clone.ts b/tests/clone.ts new file mode 100644 index 0000000..49f90a2 --- /dev/null +++ b/tests/clone.ts @@ -0,0 +1,21 @@ +import test from "tape"; +import { clone } from ".."; + +test("clone", (t) => { + t.plan(4); + + const original = { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }; + + t.deepEqual( + clone(original), + original, + "Cloning should return a graph exactly equal to the original graph", + ); + + t.notEqual(clone(original), original, "Cloning should make a copy of the graph"); + t.notEqual(clone(original).a, original.a, "Cloning should make a copy of the graph"); + t.notEqual(clone(original).b, original.b, "Cloning should make a copy of the graph"); +}); diff --git a/tests/create.ts b/tests/create.ts new file mode 100644 index 0000000..232fcdd --- /dev/null +++ b/tests/create.ts @@ -0,0 +1,15 @@ +import { create } from ".."; +import test from "tape"; + +test("create", (t) => { + t.plan(1); + + t.deepEqual( + create(2, (i) => `${i}!`), + { + "0!": { "0!": 0, "1!": 0 }, + "1!": { "0!": 0, "1!": 0 }, + }, + "Custom ID function is used", + ); +}); diff --git a/tests/degree.ts b/tests/degree.ts new file mode 100644 index 0000000..691cdd0 --- /dev/null +++ b/tests/degree.ts @@ -0,0 +1,83 @@ +import test from "tape"; +import { degree } from ".."; + +test("degree", (t) => { + t.plan(7); + + t.equal(degree({ a: { a: 0 } }, "a"), 0, "Base case"); + + t.equal( + degree( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 2, b: 0, c: 0 }, + c: { a: -0.5, b: 0, c: 0 }, + }, + "a", + ), + 3, + "The unweighted degree should be the count of edges", + ); + + t.equal( + degree( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 2, b: 0, c: 0 }, + c: { a: -0.5, b: 0, c: 0 }, + }, + "a", + { weighted: true }, + ), + 2.5, + "The weighted degree should be the sum of edge weights", + ); + + t.equal( + degree( + { + a: { a: 1.5 }, + }, + "a", + ), + 2, + "Loops should count twice towards the degree", + ); + + t.equal( + degree( + { + a: { a: 1.5 }, + }, + "a", + { weighted: true }, + ), + 3, + "Loops should count twice towards the weighted degree", + ); + + t.equal( + degree( + { + a: { a: 0, b: 1, c: 1 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }, + "a", + { undirected: true }, + ), + 2, + "Reciprocal edges should only be counted once in undirected mode", + ); + + t.throws(() => { + degree( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + "a", + { undirected: true }, + ); + }, "Using the undirected option on a directed graph should throw an error"); +}); diff --git a/tests/descendants.ts b/tests/descendants.ts new file mode 100644 index 0000000..b3741be --- /dev/null +++ b/tests/descendants.ts @@ -0,0 +1,41 @@ +import test from "tape"; +import { descendants } from ".."; + +test("descendants", (t) => { + t.plan(3); + + t.deepEqual( + descendants( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }, + "a", + ), + new Set(["b", "c"]), + ); + + t.deepEqual( + descendants( + { + a: { a: 0, b: 1, c: 0, d: 0 }, + b: { a: 0, b: 0, c: 1, d: 1 }, + c: { a: 0, b: 0, c: 0, d: 1 }, + d: { a: 0, b: 0, c: 0, d: 0 }, + }, + "a", + ), + new Set(["b", "c", "d"]), + ); + + t.throws(() => { + descendants( + { + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }, + "a", + ); + }, "A graph with cycles should throw an error"); +}); diff --git a/tests/edges.ts b/tests/edges.ts new file mode 100644 index 0000000..e1d3e24 --- /dev/null +++ b/tests/edges.ts @@ -0,0 +1,58 @@ +import test from "tape"; +import { edges } from ".."; + +test("edges", (t) => { + t.plan(4); + + t.deepEqual( + edges({ + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }), + new Set([ + ["a", "b"], + ["b", "c"], + ]), + ); + + t.deepEqual( + edges({ + a: { a: 0, b: 2, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0.5, b: 0, c: 0 }, + }), + new Set([ + ["a", "b"], + ["b", "c"], + ["c", "a"], + ]), + ); + + t.deepEqual( + edges( + { + a: { a: 1, b: 1, c: 0 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + { undirected: true }, + ), + new Set([ + ["a", "a"], + ["a", "b"], + ["b", "c"], + ]), + "Undirected mode", + ); + + t.throws(() => { + edges( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + { undirected: true }, + ); + }, "Using the undirected option on a directed graph should throw an error"); +}); diff --git a/tests/fromD3.ts b/tests/fromD3.ts new file mode 100644 index 0000000..66d76a3 --- /dev/null +++ b/tests/fromD3.ts @@ -0,0 +1,41 @@ +import test from "tape"; +import { fromD3 } from ".."; + +test("fromD3", (t) => { + t.plan(2); + + t.deepEqual( + fromD3({ + nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], + links: [ + { source: "a", target: "b" }, + { source: "a", target: "c" }, + { source: "a", target: "a" }, + ], + }), + { + a: { a: 1, b: 1, c: 1 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }, + ); + + t.deepEqual( + fromD3( + { + nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], + links: [ + { source: "a", target: "a" }, + { source: "a", target: "b" }, + { source: "a", target: "c" }, + ], + }, + { undirected: true }, + ), + { + a: { a: 1, b: 1, c: 1 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }, + ); +}); diff --git a/tests/getEdge.ts b/tests/getEdge.ts new file mode 100644 index 0000000..ca476e3 --- /dev/null +++ b/tests/getEdge.ts @@ -0,0 +1,17 @@ +import test from "tape"; +import { getEdge } from ".."; + +test("getEdge", (t) => { + t.plan(1); + + t.equal( + getEdge( + { + a: { a: 0, b: 1.5 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + ), + 1.5, + ); +}); diff --git a/tests/indegree.ts b/tests/indegree.ts new file mode 100644 index 0000000..d9dbc0f --- /dev/null +++ b/tests/indegree.ts @@ -0,0 +1,31 @@ +import test from "tape"; +import { indegree } from ".."; + +test("indegree", (t) => { + t.plan(2); + + t.equal( + indegree( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 2, b: 0, c: 0 }, + c: { a: 0.5, b: 0, c: 0 }, + }, + "a", + ), + 2, + ); + + t.equal( + indegree( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 2, b: 0, c: 0 }, + c: { a: 0.5, b: 0, c: 0 }, + }, + "a", + { weighted: true }, + ), + 2.5, + ); +}); diff --git a/tests/isCyclic.ts b/tests/isCyclic.ts new file mode 100644 index 0000000..e008550 --- /dev/null +++ b/tests/isCyclic.ts @@ -0,0 +1,128 @@ +import test from "tape"; +import { isCyclic } from ".."; + +test("isCyclic", (t) => { + t.test("directed graph", (t) => { + t.plan(9); + + t.equal(isCyclic({}), false); + + t.equal( + isCyclic({ + a: { a: 0 }, + }), + false, + ); + + t.equal( + isCyclic({ + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }), + false, + ); + + t.equal( + isCyclic({ + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }), + false, + ); + + t.equal( + isCyclic({ + a: { a: 0, b: 1, c: 1 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + false, + ); + + t.equal( + isCyclic({ + a: { a: 1 }, + }), + true, + ); + + t.equal( + isCyclic({ + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }), + true, + ); + + t.equal( + isCyclic({ + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 1, b: 0, c: 0 }, + }), + true, + ); + + t.equal( + isCyclic({ + a: { a: 1, b: 0 }, + b: { a: 0, b: 1 }, + }), + true, + ); + }); + + t.test("undirected graph", (t) => { + t.plan(5); + + t.equal(isCyclic({}, { undirected: true }), false, "Base case"); + + t.equal( + isCyclic( + { + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }, + { undirected: true }, + ), + false, + "Single edge", + ); + + t.equal( + isCyclic( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + { undirected: true }, + ), + false, + "2 edge graph", + ); + + t.equal( + isCyclic( + { + a: { a: 0, b: 1, c: 1 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 1, b: 1, c: 0 }, + }, + { undirected: true }, + ), + true, + "Complete graph", + ); + + t.throws(() => { + isCyclic( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + { undirected: true }, + ); + }, "Using the undirected option on a directed graph should throw an error"); + }); +}); diff --git a/tests/isUndirected.ts b/tests/isUndirected.ts new file mode 100644 index 0000000..aac5db0 --- /dev/null +++ b/tests/isUndirected.ts @@ -0,0 +1,26 @@ +import test from "tape"; +import { isUndirected } from ".."; + +test("isUndirected", (t) => { + t.plan(4); + + t.equal(isUndirected({}), true); + + t.equal(isUndirected({ a: { a: 1 } }), true); + + t.equal( + isUndirected({ + a: { a: 1, b: 1 }, + b: { a: 0, b: 0 }, + }), + false, + ); + + t.equal( + isUndirected({ + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }), + true, + ); +}); diff --git a/tests/makeUndirected.ts b/tests/makeUndirected.ts new file mode 100644 index 0000000..697195e --- /dev/null +++ b/tests/makeUndirected.ts @@ -0,0 +1,52 @@ +import test from "tape"; +import { makeUndirected } from ".."; + +test("makeUndirected", (t) => { + t.plan(5); + + t.deepEqual(makeUndirected({}), {}); + + t.deepEqual(makeUndirected({ a: { a: 1 } }), { a: { a: 1 } }); + + t.deepEqual( + makeUndirected({ + a: { a: 1, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + a: { a: 1, b: 1, c: 0 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + ); + + t.deepEqual( + makeUndirected({ + a: { a: 0.5, b: -1, c: 0 }, + b: { a: 0, b: 0, c: -1 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + a: { a: 0.5, b: -1, c: 0 }, + b: { a: -1, b: 0, c: -1 }, + c: { a: 0, b: -1, c: 0 }, + }, + ); + + t.deepEqual( + makeUndirected( + { + a: { a: 2, b: 3, c: 0 }, + b: { a: 2, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }, + (a, b) => a * b, + ), + { + a: { a: 2, b: 6, c: 0 }, + b: { a: 6, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + ); +}); diff --git a/tests/order.ts b/tests/order.ts new file mode 100644 index 0000000..9c1f586 --- /dev/null +++ b/tests/order.ts @@ -0,0 +1,23 @@ +import test from "tape"; +import { order } from ".."; + +test("order", (t) => { + t.plan(3); + + t.equal(order({}), 0); + + t.equal( + order({ + a: { a: 0 }, + }), + 1, + ); + + t.equal( + order({ + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }), + 2, + ); +}); diff --git a/tests/outdegree.ts b/tests/outdegree.ts new file mode 100644 index 0000000..3933b12 --- /dev/null +++ b/tests/outdegree.ts @@ -0,0 +1,31 @@ +import test from "tape"; +import { outdegree } from ".."; + +test("outdegree", (t) => { + t.plan(2); + + t.equal( + outdegree( + { + a: { a: 0, b: 2, c: 0.5 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }, + "a", + ), + 2, + ); + + t.equal( + outdegree( + { + a: { a: 0, b: 2, c: 0.5 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }, + "a", + { weighted: true }, + ), + 2.5, + ); +}); diff --git a/tests/parents.ts b/tests/parents.ts new file mode 100644 index 0000000..4377078 --- /dev/null +++ b/tests/parents.ts @@ -0,0 +1,28 @@ +import test from "tape"; +import { parents } from ".."; + +test("parents", (t) => { + t.plan(2); + + t.deepEqual( + parents( + { + a: { a: 0, b: 0, c: 1 }, + b: { a: 0, b: 0, c: 0.5 }, + c: { a: 0, b: 0, c: 0 }, + }, + "c", + ), + new Set(["a", "b"]), + ); + + t.deepEqual( + parents( + { + a: { a: 1 }, + }, + "a", + ), + new Set(["a"]), + ); +}); diff --git a/tests/removeEdge.ts b/tests/removeEdge.ts new file mode 100644 index 0000000..803036c --- /dev/null +++ b/tests/removeEdge.ts @@ -0,0 +1,52 @@ +import test from "tape"; +import { removeEdge } from ".."; + +test("removeEdge", (t) => { + t.plan(3); + + t.deepEqual( + removeEdge( + { + a: { a: 0, b: 1.5, c: 0 }, + b: { a: 0, b: 0, c: 0.5 }, + c: { a: 2, b: 0, c: 0 }, + }, + ["a", "b"], + ), + { + a: { a: 0, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0.5 }, + c: { a: 2, b: 0, c: 0 }, + }, + ); + + t.deepEqual( + removeEdge( + { + a: { a: 0, b: 2 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + ), + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + ); + + t.deepEqual( + removeEdge( + { + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }, + ["a", "b"], + { undirected: true }, + ), + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + "Undirected", + ); +}); diff --git a/tests/removeVertex.ts b/tests/removeVertex.ts new file mode 100644 index 0000000..4f1cc26 --- /dev/null +++ b/tests/removeVertex.ts @@ -0,0 +1,21 @@ +import test from "tape"; +import { removeVertex } from ".."; + +test("removeVertex", (t) => { + t.plan(1); + + t.deepEqual( + removeVertex( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 1, b: 0, c: 0 }, + }, + "b", + ), + { + a: { a: 0, c: 0 }, + c: { a: 1, c: 0 }, + }, + ); +}); diff --git a/tests/setEdge.ts b/tests/setEdge.ts new file mode 100644 index 0000000..35bfd87 --- /dev/null +++ b/tests/setEdge.ts @@ -0,0 +1,68 @@ +import test from "tape"; +import { setEdge } from ".."; + +test("setEdge", (t) => { + t.plan(4); + + t.deepEqual( + setEdge( + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + 1, + ), + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + ); + + t.deepEqual( + setEdge( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + 0, + ), + { + a: { a: 0, b: 0 }, + b: { a: 0, b: 0 }, + }, + ); + + t.deepEqual( + setEdge( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + 1.5, + ), + { + a: { a: 0, b: 1.5 }, + b: { a: 0, b: 0 }, + }, + ); + + t.deepEqual( + setEdge( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + ["a", "b"], + 1.5, + { undirected: true }, + ), + { + a: { a: 0, b: 1.5 }, + b: { a: 1.5, b: 0 }, + }, + "Undirected", + ); +}); diff --git a/tests/size.ts b/tests/size.ts new file mode 100644 index 0000000..c578b30 --- /dev/null +++ b/tests/size.ts @@ -0,0 +1,48 @@ +import test from "tape"; +import { size } from ".."; + +test("size", (t) => { + t.plan(5); + + t.equal(size({}), 0); + + t.equal( + size({ + a: { a: 1, b: 0 }, + b: { a: 0, b: 0 }, + }), + 1, + ); + + t.equal( + size({ + a: { a: 3, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }), + 2, + ); + + t.equal( + size( + { + a: { a: 0, b: 1, c: 0 }, + b: { a: 1, b: 0, c: 1 }, + c: { a: 0, b: 1, c: 0 }, + }, + { undirected: true }, + ), + 2, + "Undirected", + ); + + t.throws(() => { + size( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + { undirected: true }, + ); + }, "Using the undirected option on a directed graph should throw an error"); +}); diff --git a/tests/toD3.ts b/tests/toD3.ts new file mode 100644 index 0000000..90f6518 --- /dev/null +++ b/tests/toD3.ts @@ -0,0 +1,51 @@ +import test from "tape"; +import { toD3 } from ".."; + +test("toD3", (t) => { + t.plan(3); + + t.deepEqual( + toD3({ + a: { a: 1, b: 2, c: 0.5 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], + links: [ + { source: "a", target: "a" }, + { source: "a", target: "b" }, + { source: "a", target: "b" }, + { source: "a", target: "c" }, + ], + }, + ); + + t.deepEqual( + toD3( + { + a: { a: 0, b: 1, c: 1 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }, + { undirected: true }, + ), + { + nodes: [{ id: "a" }, { id: "b" }, { id: "c" }], + links: [ + { source: "a", target: "b" }, + { source: "a", target: "c" }, + ], + }, + ); + + t.throws(() => { + toD3( + { + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }, + { undirected: true }, + ); + }, "Using the undirected option on a directed graph should throw an error"); +}); diff --git a/tests/toDirected.ts b/tests/toDirected.ts new file mode 100644 index 0000000..0840d2b --- /dev/null +++ b/tests/toDirected.ts @@ -0,0 +1,6 @@ +import test from "tape"; + +test("toDirected", (t) => { + t.plan(1); + t.skip("TODO"); +}); diff --git a/tests/topologicalSort.ts b/tests/topologicalSort.ts new file mode 100644 index 0000000..b064b04 --- /dev/null +++ b/tests/topologicalSort.ts @@ -0,0 +1,55 @@ +import test from "tape"; +import { topologicalSort } from ".."; + +test("topologicalSort", (t) => { + t.plan(6); + + t.deepEqual( + topologicalSort({ + a: { a: 0 }, + }), + ["a"], + ); + + t.deepEqual( + topologicalSort({ + a: { a: 0, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + ["a", "b", "c"], + ); + + t.deepEqual( + topologicalSort({ + a: { a: 0, b: 1, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }), + ["c", "a", "b"], + ); + + t.deepEqual( + topologicalSort({ + a: { a: 0, b: 0, c: 0 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 1, b: 0, c: 0 }, + }), + ["b", "c", "a"], + ); + + t.deepEqual( + topologicalSort({ + a: { a: 0, b: 0 }, + b: { a: 10, b: 0 }, + }), + ["b", "a"], + ); + + t.throws(() => { + topologicalSort({ + a: { a: 0, b: 1 }, + b: { a: 1, b: 0 }, + }); + }); +}); diff --git a/tests/transpose.ts b/tests/transpose.ts new file mode 100644 index 0000000..7a35336 --- /dev/null +++ b/tests/transpose.ts @@ -0,0 +1,63 @@ +import test from "tape"; +import { transpose } from ".."; + +test("transpose", (t) => { + t.plan(5); + + t.deepEqual( + transpose({ + a: { a: 0 }, + }), + { + a: { a: 0 }, + }, + ); + + t.deepEqual( + transpose({ + a: { a: 0, b: 1 }, + b: { a: 0, b: 0 }, + }), + { + a: { a: 0, b: 0 }, + b: { a: 1, b: 0 }, + }, + ); + + t.deepEqual( + transpose({ + a: { a: 0, b: -0.5 }, + b: { a: 0, b: 0 }, + }), + { + a: { a: 0, b: 0 }, + b: { a: -0.5, b: 0 }, + }, + ); + + t.deepEqual( + transpose({ + a: { a: 0, b: 1, c: 1 }, + b: { a: 0, b: 0, c: 1 }, + c: { a: 0, b: 0, c: 0 }, + }), + { + a: { a: 0, b: 0, c: 0 }, + b: { a: 1, b: 0, c: 0 }, + c: { a: 1, b: 1, c: 0 }, + }, + ); + + t.deepEqual( + transpose({ + a: { a: 1, b: 1, c: 1 }, + b: { a: 1, b: 1, c: 1 }, + c: { a: 1, b: 1, c: 1 }, + }), + { + a: { a: 1, b: 1, c: 1 }, + b: { a: 1, b: 1, c: 1 }, + c: { a: 1, b: 1, c: 1 }, + }, + ); +}); diff --git a/tests/vertexPairs.ts b/tests/vertexPairs.ts new file mode 100644 index 0000000..2b02b94 --- /dev/null +++ b/tests/vertexPairs.ts @@ -0,0 +1,22 @@ +import test from "tape"; +import { vertexPairs } from ".."; + +test("vertexPairs", (t) => { + t.plan(1); + + t.deepEqual( + vertexPairs({ + a: { a: 0, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + new Set([ + ["a", "a"], + ["a", "b"], + ["a", "c"], + ["b", "b"], + ["b", "c"], + ["c", "c"], + ]), + ); +}); diff --git a/tests/vertices.ts b/tests/vertices.ts new file mode 100644 index 0000000..81eae2d --- /dev/null +++ b/tests/vertices.ts @@ -0,0 +1,15 @@ +import test from "tape"; +import { vertices } from ".."; + +test("vertices", (t) => { + t.plan(1); + + t.deepEqual( + vertices({ + a: { a: 0, b: 0, c: 0 }, + b: { a: 0, b: 0, c: 0 }, + c: { a: 0, b: 0, c: 0 }, + }), + new Set(["a", "b", "c"]), + ); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3a570a5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "strict": true, + "esModuleInterop": true, + "target": "es5", + "lib": ["es2020"], + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "module": "commonjs", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "downlevelIteration": true, + "incremental": true + }, + "include": ["./tests/**/*.ts", "./src/**/*.ts"] +} -- 2.51.2