Skip to content

Commit

Permalink
Implement Union for combining two graphs into one (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
dominikbraun committed Apr 15, 2023
1 parent dadf507 commit b5a223a
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
* Added the `Graph.RemoveVertex` method for removing a vertex.
* Added the `Store.RemoveVertex` method for removing a vertex.
* Added the `ErrVertexHasEdges` error instance.
* Added the `Union` function for combining two graphs into one.

## [0.17.0] - 2023-04-12

Expand Down
79 changes: 79 additions & 0 deletions sets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package graph

import (
"fmt"
)

// Union combines two given graphs into a new graph. The vertex hashes in both
// graphs are expected to be unique. The two input graphs will remain unchanged.
//
// Both graphs should be either directed or undirected. All traits for the new
// graph will be derived from g.
func Union[K comparable, T any](g, h Graph[K, T]) (Graph[K, T], error) {
union, err := g.Clone()
if err != nil {
return union, fmt.Errorf("failed to clone g: %w", err)
}

adjacencyMap, err := h.AdjacencyMap()
if err != nil {
return union, fmt.Errorf("failed to get adjacency map: %w", err)
}

addedEdges := make(map[K]map[K]struct{})

for currentHash := range adjacencyMap {
vertex, properties, err := h.VertexWithProperties(currentHash) //nolint:govet
if err != nil {
return union, fmt.Errorf("failed to get vertex %v: %w", currentHash, err)
}

err = union.AddVertex(vertex, copyVertexProperties(properties))
if err != nil {
return union, fmt.Errorf("failed to add vertex %v: %w", currentHash, err)
}
}

for _, adjacencies := range adjacencyMap {
for _, edge := range adjacencies {
if _, sourceOK := addedEdges[edge.Source]; sourceOK {
if _, targetOK := addedEdges[edge.Source][edge.Target]; targetOK {
// If the edge addedEdges[source][target] exists, the edge
// has already been created and thus can be skipped here.
continue
}
}

err = union.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
if err != nil {
return union, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err)
}

if _, ok := addedEdges[edge.Source]; !ok {
addedEdges[edge.Source] = make(map[K]struct{})
}
addedEdges[edge.Source][edge.Target] = struct{}{}
}
}

return union, nil
}

func copyVertexProperties(source VertexProperties) func(*VertexProperties) {
return func(p *VertexProperties) {
for k, v := range source.Attributes {
p.Attributes[k] = v
}
p.Weight = source.Weight
}
}

func copyEdgeProperties(source EdgeProperties) func(properties *EdgeProperties) {
return func(p *EdgeProperties) {
for k, v := range source.Attributes {
p.Attributes[k] = v
}
p.Weight = source.Weight
p.Data = source.Data
}
}
200 changes: 200 additions & 0 deletions sets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package graph

import (
"testing"
)

func TestDirectedUnion(t *testing.T) {
tests := map[string]struct {
gVertices []int
gVertexProperties map[int]VertexProperties
gEdges []Edge[int]
hVertices []int
hVertexProperties map[int]VertexProperties
hEdges []Edge[int]
expectedAdjacencyMap map[int]map[int]Edge[int]
}{
"two 3-vertices directed graphs": {
gVertices: []int{1, 2, 3},
gVertexProperties: map[int]VertexProperties{},
gEdges: []Edge[int]{
{Source: 1, Target: 2},
{Source: 2, Target: 3},
},
hVertices: []int{4, 5, 6},
hVertexProperties: map[int]VertexProperties{},
hEdges: []Edge[int]{
{Source: 4, Target: 5},
{Source: 5, Target: 6},
},
expectedAdjacencyMap: map[int]map[int]Edge[int]{
1: {
2: {Source: 1, Target: 2},
},
2: {
3: {Source: 2, Target: 3},
},
3: {},
4: {
5: {Source: 4, Target: 5},
},
5: {
6: {Source: 5, Target: 6},
},
6: {},
},
},
"vertices and edges with properties": {
gVertices: []int{1, 2},
gVertexProperties: map[int]VertexProperties{
1: {
Attributes: map[string]string{
"color": "red",
},
Weight: 10,
},
2: {
Attributes: map[string]string{},
Weight: 20,
},
},
gEdges: []Edge[int]{
{
Source: 1,
Target: 2,
Properties: EdgeProperties{
Attributes: map[string]string{
"label": "my-edge",
},
Weight: 42,
Data: "edge data #1",
},
},
},
hVertices: []int{3, 4},
hVertexProperties: map[int]VertexProperties{
3: {
Attributes: map[string]string{
"color": "blue",
},
Weight: 15,
},
},
hEdges: []Edge[int]{
{
Source: 3,
Target: 4,
Properties: EdgeProperties{
Attributes: map[string]string{
"label": "another-edge",
},
Weight: 50,
Data: "edge data #2",
},
},
},
expectedAdjacencyMap: map[int]map[int]Edge[int]{
1: {
2: {
Source: 1,
Target: 2,
Properties: EdgeProperties{
Attributes: map[string]string{
"label": "my-edge",
},
Weight: 42,
Data: "edge data #1",
},
},
},
2: {},
3: {
4: {
Source: 3,
Target: 4,
Properties: EdgeProperties{
Attributes: map[string]string{
"label": "another-edge",
},
Weight: 50,
Data: "edge data #2",
},
},
},
4: {},
},
},
}

for name, test := range tests {
g := New(IntHash, Directed())

for _, vertex := range test.gVertices {
_ = g.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex]))
}

for _, edge := range test.gEdges {
_ = g.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
}

h := New(IntHash, Directed())

for _, vertex := range test.hVertices {
_ = h.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex]))
}

for _, edge := range test.hEdges {
_ = h.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
}

union, err := Union(g, h)
if err != nil {
t.Fatalf("%s: unexpected union error: %s", name, err.Error())
}

unionAdjacencyMap, err := union.AdjacencyMap()
if err != nil {
t.Fatalf("%s: unexpected adjaceny map error: %s", name, err.Error())
}

for expectedHash, expectedAdjacencies := range test.expectedAdjacencyMap {
actualAdjacencies, ok := unionAdjacencyMap[expectedHash]
if !ok {
t.Errorf("%s: key %v doesn't exist in adjacency map", name, expectedHash)
continue
}

for expectedAdjacency, expectedEdge := range expectedAdjacencies {
actualEdge, ok := actualAdjacencies[expectedAdjacency]
if !ok {
t.Errorf("%s: key %v doesn't exist in adjacencies of %v", name, expectedAdjacency, expectedHash)
continue
}

if !union.(*directed[int, int]).edgesAreEqual(expectedEdge, actualEdge) {
t.Errorf("%s: expected edge %v, got %v at AdjacencyMap[%v][%v]", name, expectedEdge, actualEdge, expectedHash, expectedAdjacency)
}

for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
actualValue, ok := actualEdge.Properties.Attributes[expectedKey]
if !ok {
t.Errorf("%s: expected attribute %v to exist in edge %v", name, expectedKey, actualEdge)
}
if actualValue != expectedValue {
t.Errorf("%s: expected value %v for key %v in edge %v, got %v", name, expectedValue, expectedKey, expectedEdge, actualValue)
}
}

if actualEdge.Properties.Weight != expectedEdge.Properties.Weight {
t.Errorf("%s: expected weight %v for edge %v, got %v", name, expectedEdge.Properties.Weight, expectedEdge, actualEdge.Properties.Weight)
}
}
}

for actualHash := range unionAdjacencyMap {
if _, ok := test.expectedAdjacencyMap[actualHash]; !ok {
t.Errorf("%s: unexpected key %v in union adjacency map", name, actualHash)
}
}
}
}

0 comments on commit b5a223a

Please sign in to comment.