Skip to content

Commit b5a223a

Browse files
authored
Implement Union for combining two graphs into one (#100)
1 parent dadf507 commit b5a223a

File tree

3 files changed

+280
-0
lines changed

3 files changed

+280
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111
* Added the `Graph.RemoveVertex` method for removing a vertex.
1212
* Added the `Store.RemoveVertex` method for removing a vertex.
1313
* Added the `ErrVertexHasEdges` error instance.
14+
* Added the `Union` function for combining two graphs into one.
1415

1516
## [0.17.0] - 2023-04-12
1617

sets.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package graph
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
// Union combines two given graphs into a new graph. The vertex hashes in both
8+
// graphs are expected to be unique. The two input graphs will remain unchanged.
9+
//
10+
// Both graphs should be either directed or undirected. All traits for the new
11+
// graph will be derived from g.
12+
func Union[K comparable, T any](g, h Graph[K, T]) (Graph[K, T], error) {
13+
union, err := g.Clone()
14+
if err != nil {
15+
return union, fmt.Errorf("failed to clone g: %w", err)
16+
}
17+
18+
adjacencyMap, err := h.AdjacencyMap()
19+
if err != nil {
20+
return union, fmt.Errorf("failed to get adjacency map: %w", err)
21+
}
22+
23+
addedEdges := make(map[K]map[K]struct{})
24+
25+
for currentHash := range adjacencyMap {
26+
vertex, properties, err := h.VertexWithProperties(currentHash) //nolint:govet
27+
if err != nil {
28+
return union, fmt.Errorf("failed to get vertex %v: %w", currentHash, err)
29+
}
30+
31+
err = union.AddVertex(vertex, copyVertexProperties(properties))
32+
if err != nil {
33+
return union, fmt.Errorf("failed to add vertex %v: %w", currentHash, err)
34+
}
35+
}
36+
37+
for _, adjacencies := range adjacencyMap {
38+
for _, edge := range adjacencies {
39+
if _, sourceOK := addedEdges[edge.Source]; sourceOK {
40+
if _, targetOK := addedEdges[edge.Source][edge.Target]; targetOK {
41+
// If the edge addedEdges[source][target] exists, the edge
42+
// has already been created and thus can be skipped here.
43+
continue
44+
}
45+
}
46+
47+
err = union.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
48+
if err != nil {
49+
return union, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err)
50+
}
51+
52+
if _, ok := addedEdges[edge.Source]; !ok {
53+
addedEdges[edge.Source] = make(map[K]struct{})
54+
}
55+
addedEdges[edge.Source][edge.Target] = struct{}{}
56+
}
57+
}
58+
59+
return union, nil
60+
}
61+
62+
func copyVertexProperties(source VertexProperties) func(*VertexProperties) {
63+
return func(p *VertexProperties) {
64+
for k, v := range source.Attributes {
65+
p.Attributes[k] = v
66+
}
67+
p.Weight = source.Weight
68+
}
69+
}
70+
71+
func copyEdgeProperties(source EdgeProperties) func(properties *EdgeProperties) {
72+
return func(p *EdgeProperties) {
73+
for k, v := range source.Attributes {
74+
p.Attributes[k] = v
75+
}
76+
p.Weight = source.Weight
77+
p.Data = source.Data
78+
}
79+
}

sets_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package graph
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestDirectedUnion(t *testing.T) {
8+
tests := map[string]struct {
9+
gVertices []int
10+
gVertexProperties map[int]VertexProperties
11+
gEdges []Edge[int]
12+
hVertices []int
13+
hVertexProperties map[int]VertexProperties
14+
hEdges []Edge[int]
15+
expectedAdjacencyMap map[int]map[int]Edge[int]
16+
}{
17+
"two 3-vertices directed graphs": {
18+
gVertices: []int{1, 2, 3},
19+
gVertexProperties: map[int]VertexProperties{},
20+
gEdges: []Edge[int]{
21+
{Source: 1, Target: 2},
22+
{Source: 2, Target: 3},
23+
},
24+
hVertices: []int{4, 5, 6},
25+
hVertexProperties: map[int]VertexProperties{},
26+
hEdges: []Edge[int]{
27+
{Source: 4, Target: 5},
28+
{Source: 5, Target: 6},
29+
},
30+
expectedAdjacencyMap: map[int]map[int]Edge[int]{
31+
1: {
32+
2: {Source: 1, Target: 2},
33+
},
34+
2: {
35+
3: {Source: 2, Target: 3},
36+
},
37+
3: {},
38+
4: {
39+
5: {Source: 4, Target: 5},
40+
},
41+
5: {
42+
6: {Source: 5, Target: 6},
43+
},
44+
6: {},
45+
},
46+
},
47+
"vertices and edges with properties": {
48+
gVertices: []int{1, 2},
49+
gVertexProperties: map[int]VertexProperties{
50+
1: {
51+
Attributes: map[string]string{
52+
"color": "red",
53+
},
54+
Weight: 10,
55+
},
56+
2: {
57+
Attributes: map[string]string{},
58+
Weight: 20,
59+
},
60+
},
61+
gEdges: []Edge[int]{
62+
{
63+
Source: 1,
64+
Target: 2,
65+
Properties: EdgeProperties{
66+
Attributes: map[string]string{
67+
"label": "my-edge",
68+
},
69+
Weight: 42,
70+
Data: "edge data #1",
71+
},
72+
},
73+
},
74+
hVertices: []int{3, 4},
75+
hVertexProperties: map[int]VertexProperties{
76+
3: {
77+
Attributes: map[string]string{
78+
"color": "blue",
79+
},
80+
Weight: 15,
81+
},
82+
},
83+
hEdges: []Edge[int]{
84+
{
85+
Source: 3,
86+
Target: 4,
87+
Properties: EdgeProperties{
88+
Attributes: map[string]string{
89+
"label": "another-edge",
90+
},
91+
Weight: 50,
92+
Data: "edge data #2",
93+
},
94+
},
95+
},
96+
expectedAdjacencyMap: map[int]map[int]Edge[int]{
97+
1: {
98+
2: {
99+
Source: 1,
100+
Target: 2,
101+
Properties: EdgeProperties{
102+
Attributes: map[string]string{
103+
"label": "my-edge",
104+
},
105+
Weight: 42,
106+
Data: "edge data #1",
107+
},
108+
},
109+
},
110+
2: {},
111+
3: {
112+
4: {
113+
Source: 3,
114+
Target: 4,
115+
Properties: EdgeProperties{
116+
Attributes: map[string]string{
117+
"label": "another-edge",
118+
},
119+
Weight: 50,
120+
Data: "edge data #2",
121+
},
122+
},
123+
},
124+
4: {},
125+
},
126+
},
127+
}
128+
129+
for name, test := range tests {
130+
g := New(IntHash, Directed())
131+
132+
for _, vertex := range test.gVertices {
133+
_ = g.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex]))
134+
}
135+
136+
for _, edge := range test.gEdges {
137+
_ = g.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
138+
}
139+
140+
h := New(IntHash, Directed())
141+
142+
for _, vertex := range test.hVertices {
143+
_ = h.AddVertex(vertex, copyVertexProperties(test.gVertexProperties[vertex]))
144+
}
145+
146+
for _, edge := range test.hEdges {
147+
_ = h.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
148+
}
149+
150+
union, err := Union(g, h)
151+
if err != nil {
152+
t.Fatalf("%s: unexpected union error: %s", name, err.Error())
153+
}
154+
155+
unionAdjacencyMap, err := union.AdjacencyMap()
156+
if err != nil {
157+
t.Fatalf("%s: unexpected adjaceny map error: %s", name, err.Error())
158+
}
159+
160+
for expectedHash, expectedAdjacencies := range test.expectedAdjacencyMap {
161+
actualAdjacencies, ok := unionAdjacencyMap[expectedHash]
162+
if !ok {
163+
t.Errorf("%s: key %v doesn't exist in adjacency map", name, expectedHash)
164+
continue
165+
}
166+
167+
for expectedAdjacency, expectedEdge := range expectedAdjacencies {
168+
actualEdge, ok := actualAdjacencies[expectedAdjacency]
169+
if !ok {
170+
t.Errorf("%s: key %v doesn't exist in adjacencies of %v", name, expectedAdjacency, expectedHash)
171+
continue
172+
}
173+
174+
if !union.(*directed[int, int]).edgesAreEqual(expectedEdge, actualEdge) {
175+
t.Errorf("%s: expected edge %v, got %v at AdjacencyMap[%v][%v]", name, expectedEdge, actualEdge, expectedHash, expectedAdjacency)
176+
}
177+
178+
for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
179+
actualValue, ok := actualEdge.Properties.Attributes[expectedKey]
180+
if !ok {
181+
t.Errorf("%s: expected attribute %v to exist in edge %v", name, expectedKey, actualEdge)
182+
}
183+
if actualValue != expectedValue {
184+
t.Errorf("%s: expected value %v for key %v in edge %v, got %v", name, expectedValue, expectedKey, expectedEdge, actualValue)
185+
}
186+
}
187+
188+
if actualEdge.Properties.Weight != expectedEdge.Properties.Weight {
189+
t.Errorf("%s: expected weight %v for edge %v, got %v", name, expectedEdge.Properties.Weight, expectedEdge, actualEdge.Properties.Weight)
190+
}
191+
}
192+
}
193+
194+
for actualHash := range unionAdjacencyMap {
195+
if _, ok := test.expectedAdjacencyMap[actualHash]; !ok {
196+
t.Errorf("%s: unexpected key %v in union adjacency map", name, actualHash)
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)