Repository: dominikbraun/graph
Pull request #5 (2022-06-20 16:40:11)

Implement `Degree` and `DegreeByHash` functions


Patch:
diff --git a/directed.go b/directed.go
index 1d98a76..113b84f 100644
--- a/directed.go
+++ b/directed.go
@@ -225,6 +225,29 @@ func (d *directed[K, T]) CreatesCycleByHashes(sourceHash, targetHash K) (bool, e
 	return false, nil
 }
 
+func (d *directed[K, T]) Degree(vertex T) (int, error) {
+	sourceHash := d.hash(vertex)
+
+	return d.DegreeByHash(sourceHash)
+}
+
+func (d *directed[K, T]) DegreeByHash(vertexHash K) (int, error) {
+	if _, ok := d.vertices[vertexHash]; !ok {
+		return 0, fmt.Errorf("could not find vertex with hash %v", vertexHash)
+	}
+
+	degree := 0
+
+	if inEdges, ok := d.inEdges[vertexHash]; ok {
+		degree += len(inEdges)
+	}
+	if outEdges, ok := d.outEdges[vertexHash]; ok {
+		degree += len(outEdges)
+	}
+
+	return degree, nil
+}
+
 func (d *directed[K, T]) edgesAreEqual(a, b Edge[T]) bool {
 	aSourceHash := d.hash(a.Source)
 	aTargetHash := d.hash(a.Target)
diff --git a/graph.go b/graph.go
index 57bbbb5..6a9a874 100644
--- a/graph.go
+++ b/graph.go
@@ -126,6 +126,12 @@ type Graph[K comparable, T any] interface {
 	// CreatesCycleByHashes does the same as CreatesCycle, but uses a hash value to identify the
 	// starting vertex.
 	CreatesCycleByHashes(sourceHash, targetHash K) (bool, error)
+
+	// Degree determines and returns the degree of a given vertex.
+	Degree(vertex T) (int, error)
+
+	// DegreeByHash does the same as Degree, but uses a hash value to identify the vertex.
+	DegreeByHash(vertexHash K) (int, error)
 }
 
 // Edge represents a graph edge with a source and target vertex as well as a weight, which has the
diff --git a/undirected.go b/undirected.go
index 7993eca..46b4e93 100644
--- a/undirected.go
+++ b/undirected.go
@@ -232,6 +232,29 @@ func (u *undirected[K, T]) CreatesCycleByHashes(sourceHash, targetHash K) (bool,
 	return false, nil
 }
 
+func (u *undirected[K, T]) Degree(vertex T) (int, error) {
+	sourceHash := u.hash(vertex)
+
+	return u.DegreeByHash(sourceHash)
+}
+
+func (u *undirected[K, T]) DegreeByHash(vertexHash K) (int, error) {
+	if _, ok := u.vertices[vertexHash]; !ok {
+		return 0, fmt.Errorf("could not find vertex with hash %v", vertexHash)
+	}
+
+	degree := 0
+
+	// Adding the number of ingoing edges is sufficient for undirected graphs, because all edges
+	// exist twice (as two directed edges in opposite directions). Either dividing the number of
+	// ingoing + outgoing edges by 2 or just using the number of ingoing edges is appropriate.
+	if inEdges, ok := u.inEdges[vertexHash]; ok {
+		degree += len(inEdges)
+	}
+
+	return degree, nil
+}
+
 func (u *undirected[K, T]) edgesAreEqual(a, b Edge[T]) bool {
 	aSourceHash := u.hash(a.Source)
 	aTargetHash := u.hash(a.Target)


Test patch:
diff --git a/directed_test.go b/directed_test.go
index 49a6f5c..c0ad349 100644
--- a/directed_test.go
+++ b/directed_test.go
@@ -405,6 +405,44 @@ func TestDirected_CreatesCycleByHashes(t *testing.T) {
 	}
 }
 
+func TestDirected_Degree(t *testing.T) {
+	TestDirected_DegreeByHash(t)
+}
+
+func TestDirected_DegreeByHash(t *testing.T) {
+	tests := map[string]struct {
+		vertices       []int
+		edges          []Edge[int]
+		vertexHash     int
+		expectedDegree int
+		shouldFail     bool
+	}{}
+
+	for name, test := range tests {
+		graph := newDirected(IntHash, &properties{})
+
+		for _, vertex := range test.vertices {
+			graph.Vertex(vertex)
+		}
+
+		for _, edge := range test.edges {
+			if err := graph.Edge(edge.Source, edge.Target); err != nil {
+				t.Fatalf("%s: failed to add edge: %s", name, err.Error())
+			}
+		}
+
+		degree, err := graph.DegreeByHash(test.vertexHash)
+
+		if test.shouldFail != (err != nil) {
+			t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err)
+		}
+
+		if degree != test.expectedDegree {
+			t.Errorf("%s: degree expectancy doesn't match: expcted %v, got %v", name, test.expectedDegree, degree)
+		}
+	}
+}
+
 func TestDirected_edgesAreEqual(t *testing.T) {
 	tests := map[string]struct {
 		a             Edge[int]
diff --git a/undirected_test.go b/undirected_test.go
index 46a364e..c366480 100644
--- a/undirected_test.go
+++ b/undirected_test.go
@@ -456,6 +456,44 @@ func TestUndirected_CreatesCycleByHashes(t *testing.T) {
 	}
 }
 
+func TestUndirected_Degree(t *testing.T) {
+	TestDirected_Degree(t)
+}
+
+func TestUndirected_DegreeByHash(t *testing.T) {
+	tests := map[string]struct {
+		vertices       []int
+		edges          []Edge[int]
+		vertexHash     int
+		expectedDegree int
+		shouldFail     bool
+	}{}
+
+	for name, test := range tests {
+		graph := newUndirected(IntHash, &properties{})
+
+		for _, vertex := range test.vertices {
+			graph.Vertex(vertex)
+		}
+
+		for _, edge := range test.edges {
+			if err := graph.Edge(edge.Source, edge.Target); err != nil {
+				t.Fatalf("%s: failed to add edge: %s", name, err.Error())
+			}
+		}
+
+		degree, err := graph.DegreeByHash(test.vertexHash)
+
+		if test.shouldFail != (err != nil) {
+			t.Fatalf("%s: error expectancy doesn't match: expected %v, got %v (error: %v)", name, test.shouldFail, (err != nil), err)
+		}
+
+		if degree != test.expectedDegree {
+			t.Errorf("%s: degree expectancy doesn't match: expcted %v, got %v", name, test.expectedDegree, degree)
+		}
+	}
+}
+
 func TestUndirected_edgesAreEqual(t *testing.T) {
 	tests := map[string]struct {
 		a             Edge[int]