Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions directed.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,77 @@ func (d *directed[K, T]) AddVertex(value T, options ...func(*VertexProperties))
return d.store.AddVertex(hash, value, properties)
}

func (d *directed[K, T]) UpdateVertex(existingHash K, value T, options ...func(*VertexProperties)) error {
_, _, err := d.store.Vertex(existingHash)
if err != nil {
return err
}

newHash := d.hash(value)
if existingHash != newHash {
_, _, err = d.store.Vertex(newHash)
if !errors.Is(err, ErrVertexNotFound) {
return ErrVertexAlreadyExists
}
}

adjacencyMap, err := d.AdjacencyMap()
if err != nil {
return err
}

existingEdges := adjacencyMap[existingHash]

for _, edge := range existingEdges {
err = d.RemoveEdge(edge.Source, edge.Target)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For anyone who comes across this later - incoming edges (the ones in PredecessorMap()) also need to be deleted for RemoveVertex to work. Afaik the adjacencyMap only has outgoing ones

if err != nil {
return err
}
}

err = d.store.RemoveVertex(existingHash)
if err != nil {
return err
}

properties := VertexProperties{
Weight: 0,
Attributes: make(map[string]string),
}

for _, option := range options {
option(&properties)
}

err = d.store.AddVertex(newHash, value, properties)
if err != nil {
return err
}

for _, existingEdge := range existingEdges {
src := existingEdge.Source
if existingEdge.Source == existingHash {
src = newHash
}
tgt := existingEdge.Target
if existingEdge.Target == existingHash {
tgt = newHash
}

edge := Edge[K]{
Source: src,
Target: tgt,
Properties: existingEdge.Properties,
}

if err := d.addEdge(src, tgt, edge); err != nil {
return err
}
}

return nil
}

func (d *directed[K, T]) AddVerticesFrom(g Graph[K, T]) error {
adjacencyMap, err := g.AdjacencyMap()
if err != nil {
Expand Down
138 changes: 138 additions & 0 deletions directed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,144 @@ func TestDirected_RemoveVertex(t *testing.T) {
}
}

func TestDirected_UpdateVertex(t *testing.T) {
tests := map[string]struct {
vertices []int
properties *VertexProperties
edges []Edge[int]
updateVertex struct {
existingHash int
vertex int
properties *VertexProperties
}
expectedVertices []int
expectedProperties *VertexProperties
expectedEdges []Edge[int]
expectedErr error
}{
"update a vertex": {
vertices: []int{1, 2},
edges: []Edge[int]{
{
Source: 1,
Target: 2,
Properties: EdgeProperties{
Weight: 10,
Attributes: map[string]string{
"color": "red",
},
Data: "my-edge",
},
},
},
updateVertex: struct {
existingHash int
vertex int
properties *VertexProperties
}{
existingHash: 1,
vertex: 100,
properties: &VertexProperties{
Weight: 20,
Attributes: map[string]string{
"color": "blue",
"label": "a blue edge",
},
},
},
expectedVertices: []int{100, 2},
expectedEdges: []Edge[int]{
{
Source: 100,
Target: 2,
Properties: EdgeProperties{
Weight: 10,
Attributes: map[string]string{
"color": "red",
},
Data: "my-edge",
},
},
},
},
}

for name, test := range tests {
t.Run(name, func(t *testing.T) {
graph := newUndirected(IntHash, &Traits{}, newMemoryStore[int, int]())

var err error

for _, vertex := range test.vertices {
if test.properties == nil {
err = graph.AddVertex(vertex)
continue
}
// If there are vertex attributes, iterate over them and call the
// VertexAttribute functional option for each entry. A vertex should
// only have one attribute so that AddVertex is invoked once.
for key, value := range test.properties.Attributes {
err = graph.AddVertex(vertex, VertexWeight(test.properties.Weight), VertexAttribute(key, value))
}
}

for _, edge := range test.edges {
_ = graph.AddEdge(copyEdge(edge))
}

if test.updateVertex.properties == nil {
err = graph.UpdateVertex(test.updateVertex.existingHash, test.updateVertex.vertex)
} else {
options := []func(*VertexProperties){VertexWeight(test.updateVertex.properties.Weight)}
for key, value := range test.updateVertex.properties.Attributes {
options = append(options, VertexAttribute(key, value))
}
err = graph.UpdateVertex(test.updateVertex.existingHash, test.updateVertex.vertex, options...)
}
if !errors.Is(err, test.expectedErr) {
t.Fatalf("expected error %v, got %v", test.expectedErr, err)
}

graphStore := graph.store.(*memoryStore[int, int])

for _, expectedVertex := range test.expectedVertices {
if len(graphStore.vertices) != len(test.expectedVertices) {
t.Errorf("%s: vertex count doesn't match: expected %v, got %v", name, len(test.expectedVertices), len(graphStore.vertices))
}

hash := graph.hash(expectedVertex)
vertices := graph.store.(*memoryStore[int, int]).vertices
if _, ok := vertices[hash]; !ok {
t.Errorf("%s: vertex %v not found in graph: %v", name, expectedVertex, vertices)
}

if test.properties == nil {
continue
}

if graphStore.vertexProperties[hash].Weight != test.expectedProperties.Weight {
t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, test.expectedProperties.Weight, graphStore.vertexProperties[hash].Weight)
}

if len(graphStore.vertexProperties[hash].Attributes) != len(test.expectedProperties.Attributes) {
t.Fatalf("%s: attributes lengths don't match: expcted %v, got %v", name, len(test.expectedProperties.Attributes), len(graphStore.vertexProperties[hash].Attributes))
}
}

for _, expectedEdge := range test.expectedEdges {
actualEdge, err := graph.Edge(expectedEdge.Source, expectedEdge.Target)
if err != nil {
t.Fatalf("unexpected error: %v", err.Error())
}

if !edgesAreEqual(expectedEdge, actualEdge, false) {
t.Errorf("expected edge %v, got %v", expectedEdge, actualEdge)
}
}
})
}
}

func TestDirected_AddEdge(t *testing.T) {
tests := map[string]struct {
vertices []int
Expand Down
4 changes: 4 additions & 0 deletions graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ type Graph[K comparable, T any] interface {
//
AddVertex(value T, options ...func(*VertexProperties)) error

// UpdateVertex updates an existing vertex in the graph. If the vertex doesn't
// exist in the graph, ErrVertexNotFound will be returned.
UpdateVertex(hash K, value T, options ...func(*VertexProperties)) error

// AddVerticesFrom adds all vertices along with their properties from the
// given graph to the receiving graph.
//
Expand Down
71 changes: 71 additions & 0 deletions undirected.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,77 @@ func (u *undirected[K, T]) AddVertex(value T, options ...func(*VertexProperties)
return u.store.AddVertex(hash, value, prop)
}

func (u *undirected[K, T]) UpdateVertex(existingHash K, value T, options ...func(*VertexProperties)) error {
_, _, err := u.store.Vertex(existingHash)
if err != nil {
return err
}

newHash := u.hash(value)
if existingHash != newHash {
_, _, err = u.store.Vertex(newHash)
if !errors.Is(err, ErrVertexNotFound) {
return ErrVertexAlreadyExists
}
}

adjacencyMap, err := u.AdjacencyMap()
if err != nil {
return err
}

existingEdges := adjacencyMap[existingHash]

for _, edge := range existingEdges {
err = u.RemoveEdge(edge.Source, edge.Target)
if err != nil {
return err
}
}

err = u.store.RemoveVertex(existingHash)
if err != nil {
return err
}

properties := VertexProperties{
Weight: 0,
Attributes: make(map[string]string),
}

for _, option := range options {
option(&properties)
}

err = u.store.AddVertex(newHash, value, properties)
if err != nil {
return err
}

for _, existingEdge := range existingEdges {
src := existingEdge.Source
if existingEdge.Source == existingHash {
src = newHash
}
tgt := existingEdge.Target
if existingEdge.Target == existingHash {
tgt = newHash
}

edge := Edge[K]{
Source: src,
Target: tgt,
Properties: existingEdge.Properties,
}

if err := u.addEdge(src, tgt, edge); err != nil {
return err
}
}

return nil
}

func (u *undirected[K, T]) Vertex(hash K) (T, error) {
vertex, _, err := u.store.Vertex(hash)
return vertex, err
Expand Down
Loading