From 9f8e121468ccc62068d524bc26f4429194ba0218 Mon Sep 17 00:00:00 2001 From: Dennis Kats Date: Tue, 12 Dec 2023 21:07:00 -0500 Subject: [PATCH] Added higher-level API for querying a node's degree in a graph (#75) * improved degree api and added tests * fixed typo in `TotalDegree()` documentation --- cgraph/cgraph.go | 36 +++++++++++++++++++++++++++ graphviz_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/cgraph/cgraph.go b/cgraph/cgraph.go index 0f0e9e1..4a44d71 100644 --- a/cgraph/cgraph.go +++ b/cgraph/cgraph.go @@ -1117,10 +1117,46 @@ func (g *Graph) NumberSubGraph() int { return ccall.Agnsubg(g.Agraph) } +// Returns the degree of the given node in the graph, where arguments "in" and +// "out" are C-like booleans that select which edge sets to query. +// +// g.Degree(node, 0, 0) // always returns 0 +// g.Degree(node, 0, 1) // returns the node's outdegree +// g.Degree(node, 1, 0) // returns the node's indegree +// g.Degree(node, 1, 1) // returns the node's total degree (indegree + outdegree) func (g *Graph) Degree(n *Node, in, out int) int { return ccall.Agdegree(g.Agraph, n.Agnode, in, out) } +// Returns the indegree of the given node in the graph. +// +// Note: While undirected graphs don't normally have a +// notion of indegrees, calling this method on an +// undirected graph will treat it as if it's directed. +// As a result, it's best to avoid calling this method +// on an undirected graph. +func (g *Graph) Indegree(n *Node) int { + return ccall.Agdegree(g.Agraph, n.Agnode, 1, 0) +} + +// Returns the outdegree of the given node in the graph. +// +// Note: While undirected graphs don't normally have a +// notion of outdegrees, calling this method on an +// undirected graph will treat it as if it's directed. +// As a result, it's best to avoid calling this method +// on an undirected graph. +func (g *Graph) Outdegree(n *Node) int { + return ccall.Agdegree(g.Agraph, n.Agnode, 0, 1) +} + +// Returns the total degree of the given node in the graph. +// This can be thought of as the total number of edges coming +// in and out of a node. +func (g *Graph) TotalDegree(n *Node) int { + return ccall.Agdegree(g.Agraph, n.Agnode, 1, 1) +} + func (g *Graph) CountUniqueEdges(n *Node, in, out int) int { return ccall.Agcountuniqedges(g.Agraph, n.Agnode, in, out) } diff --git a/graphviz_test.go b/graphviz_test.go index 2984fc1..c43c2b4 100644 --- a/graphviz_test.go +++ b/graphviz_test.go @@ -149,3 +149,68 @@ func TestParseFile(t *testing.T) { } } } + +func TestNodeDegree(t *testing.T) { + type test struct { + node_name string + expected_indegree int + expected_outdegree int + expected_total_degree int + } + + type graphtest struct { + input string + tests []test + } + + graphtests := []graphtest{ + {input: "digraph test { a -> b }", tests: []test{ + {node_name: "a", expected_indegree: 0, expected_outdegree: 1, expected_total_degree: 1}, + {node_name: "b", expected_indegree: 1, expected_outdegree: 0, expected_total_degree: 1}, + }}, + {input: "digraph test { a -> b; a -> b; a -> a; c -> a }", tests: []test{ + {node_name: "a", expected_indegree: 2, expected_outdegree: 3, expected_total_degree: 5}, + {node_name: "b", expected_indegree: 2, expected_outdegree: 0, expected_total_degree: 2}, + {node_name: "c", expected_indegree: 0, expected_outdegree: 1, expected_total_degree: 1}, + }}, + {input: "graph test { a -- b; a -- b; a -- a; c -- a }", tests: []test{ + {node_name: "a", expected_indegree: 2, expected_outdegree: 3, expected_total_degree: 5}, + {node_name: "b", expected_indegree: 2, expected_outdegree: 0, expected_total_degree: 2}, + {node_name: "c", expected_indegree: 0, expected_outdegree: 1, expected_total_degree: 1}, + }}, + {input: "strict graph test { a -- b; b -- a; a -- a; c -- a }", tests: []test{ + {node_name: "a", expected_indegree: 2, expected_outdegree: 2, expected_total_degree: 4}, + {node_name: "b", expected_indegree: 1, expected_outdegree: 0, expected_total_degree: 1}, + {node_name: "c", expected_indegree: 0, expected_outdegree: 1, expected_total_degree: 1}, + }}, + } + + for _, graphtest := range graphtests { + input := graphtest.input + graph, err := graphviz.ParseBytes([]byte(input)) + if err != nil { + t.Fatalf("Input: %s. Error: %+v", input, err) + } + + for _, test := range graphtest.tests { + node_name := test.node_name + node, err := graph.Node(node_name) + if err != nil || node == nil { + t.Fatalf("Unable to retrieve node '%s'. Input: %s. Error: %+v", node_name, input, err) + } + + indegree := graph.Indegree(node) + if test.expected_indegree != indegree { + t.Errorf("Unexpected indegree for node '%s'. Input: %s. Expected: %d. Actual: %d.", node_name, input, test.expected_indegree, indegree) + } + outdegree := graph.Outdegree(node) + if test.expected_outdegree != outdegree { + t.Errorf("Unexpected outdegree for node '%s'. Input: %s. Expected: %d. Actual: %d.", node_name, input, test.expected_outdegree, outdegree) + } + total_degree := graph.TotalDegree(node) + if test.expected_total_degree != total_degree { + t.Errorf("Unexpected total degree for node '%s'. Input: %s. Expected: %d. Actual: %d.", node_name, input, test.expected_total_degree, total_degree) + } + } + } +}