diff --git a/directed.go b/directed.go index a044194..e965ac9 100644 --- a/directed.go +++ b/directed.go @@ -1,315 +1,45 @@ package graph -import ( - "errors" - "fmt" -) - type directed[K comparable, T any] struct { - hash Hash[K, T] - traits *Traits - store Store[K, T] + graph[K, T] } func newDirected[K comparable, T any](hash Hash[K, T], traits *Traits, store Store[K, T]) *directed[K, T] { + traits.IsDirected = true return &directed[K, T]{ - hash: hash, - traits: traits, - store: store, - } -} - -func (d *directed[K, T]) Traits() *Traits { - return d.traits -} - -func (d *directed[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error { - hash := d.hash(value) - properties := VertexProperties{ - Weight: 0, - Attributes: make(map[string]string), - } - - for _, option := range options { - option(&properties) + graph: graph[K, T]{ + hash: hash, + traits: traits, + store: store, + }, } - - return d.store.AddVertex(hash, value, properties) } func (d *directed[K, T]) AddVerticesFrom(g Graph[K, T]) error { - adjacencyMap, err := g.AdjacencyMap() - if err != nil { - return fmt.Errorf("failed to get adjacency map: %w", err) - } - - for hash := range adjacencyMap { - vertex, properties, err := g.VertexWithProperties(hash) - if err != nil { - return fmt.Errorf("failed to get vertex %v: %w", hash, err) - } - - if err = d.AddVertex(vertex, copyVertexProperties(properties)); err != nil { - return fmt.Errorf("failed to add vertex %v: %w", hash, err) - } - } - - return nil -} - -func (d *directed[K, T]) Vertex(hash K) (T, error) { - vertex, _, err := d.store.Vertex(hash) - return vertex, err -} - -func (d *directed[K, T]) VertexWithProperties(hash K) (T, VertexProperties, error) { - vertex, properties, err := d.store.Vertex(hash) - if err != nil { - return vertex, VertexProperties{}, err - } - - return vertex, properties, nil -} - -func (d *directed[K, T]) RemoveVertex(hash K) error { - return d.store.RemoveVertex(hash) -} - -func (d *directed[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error { - _, _, err := d.store.Vertex(sourceHash) - if err != nil { - return fmt.Errorf("source vertex %v: %w", sourceHash, err) - } - - _, _, err = d.store.Vertex(targetHash) - if err != nil { - return fmt.Errorf("target vertex %v: %w", targetHash, err) - } - - if _, err := d.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) { - return ErrEdgeAlreadyExists - } + other := tograph(g) + return d.addVerticesFrom(&other) - // If the user opted in to preventing cycles, run a cycle check. - if d.traits.PreventCycles { - createsCycle, err := d.createsCycle(sourceHash, targetHash) - if err != nil { - return fmt.Errorf("check for cycles: %w", err) - } - if createsCycle { - return ErrEdgeCreatesCycle - } - } - - edge := Edge[K]{ - Source: sourceHash, - Target: targetHash, - Properties: EdgeProperties{ - Attributes: make(map[string]string), - }, - } - - for _, option := range options { - option(&edge.Properties) - } - - return d.addEdge(sourceHash, targetHash, edge) } func (d *directed[K, T]) AddEdgesFrom(g Graph[K, T]) error { - edges, err := g.Edges() - if err != nil { - return fmt.Errorf("failed to get edges: %w", err) - } - - for _, edge := range edges { - if err := d.AddEdge(copyEdge(edge)); err != nil { - return fmt.Errorf("failed to add (%v, %v): %w", edge.Source, edge.Target, err) - } - } - - return nil -} - -func (d *directed[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) { - edge, err := d.store.Edge(sourceHash, targetHash) - if err != nil { - return Edge[T]{}, err - } - - sourceVertex, _, err := d.store.Vertex(sourceHash) - if err != nil { - return Edge[T]{}, err - } - - targetVertex, _, err := d.store.Vertex(targetHash) - if err != nil { - return Edge[T]{}, err - } - - return Edge[T]{ - Source: sourceVertex, - Target: targetVertex, - Properties: EdgeProperties{ - Weight: edge.Properties.Weight, - Attributes: edge.Properties.Attributes, - Data: edge.Properties.Data, - }, - }, nil -} - -func (d *directed[K, T]) Edges() ([]Edge[K], error) { - return d.store.ListEdges() + other := tograph(g) + return d.addEdgesFrom(&other) } func (d *directed[K, T]) UpdateEdge(source, target K, options ...func(properties *EdgeProperties)) error { - existingEdge, err := d.store.Edge(source, target) - if err != nil { - return err - } - - for _, option := range options { - option(&existingEdge.Properties) - } - - return d.store.UpdateEdge(source, target, existingEdge) -} - -func (d *directed[K, T]) RemoveEdge(source, target K) error { - if _, err := d.Edge(source, target); err != nil { - return err - } - - if err := d.store.RemoveEdge(source, target); err != nil { - return fmt.Errorf("failed to remove edge from %v to %v: %w", source, target, err) - } - - return nil -} - -func (d *directed[K, T]) AdjacencyMap() (map[K]map[K]Edge[K], error) { - vertices, err := d.store.ListVertices() - if err != nil { - return nil, fmt.Errorf("failed to list vertices: %w", err) - } + _, err := d.updateEdge(source, target, options...) - edges, err := d.store.ListEdges() - if err != nil { - return nil, fmt.Errorf("failed to list edges: %w", err) - } - - m := make(map[K]map[K]Edge[K], len(vertices)) - - for _, vertex := range vertices { - m[vertex] = make(map[K]Edge[K]) - } - - for _, edge := range edges { - m[edge.Source][edge.Target] = edge - } - - return m, nil -} - -func (d *directed[K, T]) PredecessorMap() (map[K]map[K]Edge[K], error) { - vertices, err := d.store.ListVertices() - if err != nil { - return nil, fmt.Errorf("failed to list vertices: %w", err) - } - - edges, err := d.store.ListEdges() - if err != nil { - return nil, fmt.Errorf("failed to list edges: %w", err) - } - - m := make(map[K]map[K]Edge[K], len(vertices)) - - for _, vertex := range vertices { - m[vertex] = make(map[K]Edge[K]) - } - - for _, edge := range edges { - if _, ok := m[edge.Target]; !ok { - m[edge.Target] = make(map[K]Edge[K]) - } - m[edge.Target][edge.Source] = edge - } - - return m, nil -} - -func (d *directed[K, T]) addEdge(sourceHash, targetHash K, edge Edge[K]) error { - return d.store.AddEdge(sourceHash, targetHash, edge) + return err } func (d *directed[K, T]) Clone() (Graph[K, T], error) { - traits := &Traits{ - IsDirected: d.traits.IsDirected, - IsAcyclic: d.traits.IsAcyclic, - IsWeighted: d.traits.IsWeighted, - IsRooted: d.traits.IsRooted, - PreventCycles: d.traits.PreventCycles, - } - - clone := &directed[K, T]{ - hash: d.hash, - traits: traits, - store: newMemoryStore[K, T](), - } - - if err := clone.AddVerticesFrom(d); err != nil { - return nil, fmt.Errorf("failed to add vertices: %w", err) - } - - if err := clone.AddEdgesFrom(d); err != nil { - return nil, fmt.Errorf("failed to add edges: %w", err) - } - - return clone, nil -} - -func (d *directed[K, T]) Order() (int, error) { - return d.store.VertexCount() -} - -func (d *directed[K, T]) Size() (int, error) { - return d.store.EdgeCount() -} - -func (d *directed[K, T]) edgesAreEqual(a, b Edge[T]) bool { - aSourceHash := d.hash(a.Source) - aTargetHash := d.hash(a.Target) - bSourceHash := d.hash(b.Source) - bTargetHash := d.hash(b.Target) - - return aSourceHash == bSourceHash && aTargetHash == bTargetHash -} - -func (d *directed[K, T]) createsCycle(source, target K) (bool, error) { - // If the underlying store implements CreatesCycle, use that fast path. - if cc, ok := d.store.(interface { - CreatesCycle(source, target K) (bool, error) - }); ok { - return cc.CreatesCycle(source, target) + gclone, err := d.clone() + if err != nil { + return nil, err } - // Slow path. - return CreatesCycle(Graph[K, T](d), source, target) -} - -// copyEdge returns an argument list suitable for the Graph.AddEdge method. This -// argument list is derived from the given edge, hence the name copyEdge. -// -// The last argument is a custom functional option that sets the edge properties -// to the properties of the original edge. -func copyEdge[K comparable](edge Edge[K]) (K, K, func(properties *EdgeProperties)) { - copyProperties := func(p *EdgeProperties) { - for k, v := range edge.Properties.Attributes { - p.Attributes[k] = v - } - p.Weight = edge.Properties.Weight - p.Data = edge.Properties.Data + dir := &directed[K, T]{ + graph: *gclone, } - - return edge.Source, edge.Target, copyProperties + return dir, nil } diff --git a/directed_test.go b/directed_test.go index 34a9c8d..4d965cc 100644 --- a/directed_test.go +++ b/directed_test.go @@ -10,10 +10,6 @@ func TestDirected_Traits(t *testing.T) { traits *Traits expected *Traits }{ - "default traits": { - traits: &Traits{}, - expected: &Traits{}, - }, "directed": { traits: &Traits{IsDirected: true}, expected: &Traits{IsDirected: true}, diff --git a/graph.go b/graph.go index 9376eb5..9c15a92 100644 --- a/graph.go +++ b/graph.go @@ -49,7 +49,10 @@ // For detailed usage examples, take a look at the README. package graph -import "errors" +import ( + "errors" + "fmt" +) var ( ErrVertexNotFound = errors.New("vertex not found") @@ -289,15 +292,9 @@ func NewLike[K comparable, T any](g Graph[K, T]) Graph[K, T] { t.PreventCycles = g.Traits().PreventCycles } - var hash Hash[K, T] - - if g.Traits().IsDirected { - hash = g.(*directed[K, T]).hash - } else { - hash = g.(*undirected[K, T]).hash - } + gr := tograph(g) - return New(hash, copyTraits) + return New(gr.hash, copyTraits) } // StringHash is a hashing function that accepts a string and uses that exact @@ -385,3 +382,421 @@ func VertexAttributes(attributes map[string]string) func(*VertexProperties) { e.Attributes = attributes } } + +// graph is a partial Graph[K, T] implementation where all the operations are +// common to both directed and undirected graphs. +type graph[K comparable, T any] struct { + hash Hash[K, T] + traits *Traits + store Store[K, T] +} + +func (g *graph[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error { + edge, err := g.addEdgeWithOptions(sourceHash, targetHash, options...) + if err != nil || g.traits.IsDirected { // if we're directed or in error, we're done + return err + } + + // add reverse edge of undirected + rEdge := Edge[K]{ + Source: edge.Target, + Target: edge.Source, + Properties: EdgeProperties{ + Weight: edge.Properties.Weight, + Attributes: edge.Properties.Attributes, + Data: edge.Properties.Data, + }, + } + + return g.store.AddEdge(targetHash, sourceHash, rEdge) +} + +func (g *graph[K, T]) addEdge(sourceHash, targetHash K, edge Edge[K]) error { + return g.store.AddEdge(sourceHash, targetHash, edge) +} + +func (g *graph[K, T]) addEdgeWithOptions(sourceHash, targetHash K, options ...func(*EdgeProperties)) (Edge[K], error) { + if _, _, err := g.store.Vertex(sourceHash); err != nil { + return Edge[K]{}, fmt.Errorf("could not find source vertex with hash %v: %w", sourceHash, err) + } + + if _, _, err := g.store.Vertex(targetHash); err != nil { + return Edge[K]{}, fmt.Errorf("could not find target vertex with hash %v: %w", targetHash, err) + } + + //nolint:govet // False positive. + if _, err := g.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) { + return Edge[K]{}, ErrEdgeAlreadyExists + } + + // If the user opted in to preventing cycles, run a cycle check. + if g.traits.PreventCycles { + createsCycle, err := g.createsCycle(sourceHash, targetHash) + if err != nil { + return Edge[K]{}, fmt.Errorf("check for cycles: %w", err) + } + if createsCycle { + return Edge[K]{}, ErrEdgeCreatesCycle + } + } + + edge := Edge[K]{ + Source: sourceHash, + Target: targetHash, + Properties: EdgeProperties{ + Attributes: make(map[string]string), + }, + } + + for _, option := range options { + option(&edge.Properties) + } + + if err := g.store.AddEdge(sourceHash, targetHash, edge); err != nil { + return Edge[K]{}, fmt.Errorf("failed to add edge: %w", err) + } + + return edge, nil +} + +func (g *graph[K, T]) addEdgesFrom(other *graph[K, T]) error { + edges, err := other.Edges() + if err != nil { + return fmt.Errorf("failed to get edges: %w", err) + } + + for _, edge := range edges { + if err := g.AddEdge(copyEdge(edge)); err != nil { + return fmt.Errorf("failed to add (%v, %v): %w", edge.Source, edge.Target, err) + } + } + + return nil +} + +func (g *graph[K, T]) clone() (*graph[K, T], error) { + traits := &Traits{ + IsDirected: g.traits.IsDirected, + IsAcyclic: g.traits.IsAcyclic, + IsWeighted: g.traits.IsWeighted, + IsRooted: g.traits.IsRooted, + PreventCycles: g.traits.PreventCycles, + } + + graph := &graph[K, T]{ + hash: g.hash, + traits: traits, + store: newMemoryStore[K, T](), + } + + if err := graph.addVerticesFrom(g); err != nil { + return nil, fmt.Errorf("failed to add vertices: %w", err) + } + + if err := graph.addEdgesFrom(g); err != nil { + return nil, fmt.Errorf("failed to add edges: %w", err) + } + + return graph, nil +} +func (g *graph[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) { + edge, err := g.store.Edge(sourceHash, targetHash) + if !g.traits.IsDirected { + // In an undirected graph, since multigraphs aren't supported, the edge AB + // is the same as BA. Therefore, if source[target] cannot be found, this + // function also looks for target[source]. + if errors.Is(err, ErrEdgeNotFound) { + edge, err = g.store.Edge(targetHash, sourceHash) + } + } + + if err != nil { + return Edge[T]{}, err + } + + sourceVertex, _, err := g.store.Vertex(sourceHash) + if err != nil { + return Edge[T]{}, err + } + + targetVertex, _, err := g.store.Vertex(targetHash) + if err != nil { + return Edge[T]{}, err + } + + return Edge[T]{ + Source: sourceVertex, + Target: targetVertex, + Properties: EdgeProperties{ + Weight: edge.Properties.Weight, + Attributes: edge.Properties.Attributes, + Data: edge.Properties.Data, + }, + }, nil +} + +type tuple[K comparable] struct { + source, target K +} + +func (g *graph[K, T]) Edges() ([]Edge[K], error) { + storedEdges, err := g.store.ListEdges() + if g.traits.IsDirected { + return storedEdges, err + } + // An undirected graph creates each edge twice internally: The edge (A,B) is + // stored both as (A,B) and (B,A). The Edges method is supposed to return + // one of these two edges, because from an outside perspective, it only is + // a single edge. + // + // To achieve this, Edges keeps track of already-added edges. For each edge, + // it also checks if the reversed edge has already been added - e.g., for + // an edge (A,B), Edges checks if the edge has been added as (B,A). + // + // These reversed edges are built as a custom tuple type, which is then used + // as a map key for access in O(1) time. It looks scarier than it is. + edges := make([]Edge[K], 0, len(storedEdges)/2) + + added := make(map[tuple[K]]struct{}) + + for _, storedEdge := range storedEdges { + reversedEdge := tuple[K]{ + source: storedEdge.Target, + target: storedEdge.Source, + } + if _, ok := added[reversedEdge]; ok { + continue + } + + edges = append(edges, storedEdge) + + addedEdge := tuple[K]{ + source: storedEdge.Source, + target: storedEdge.Target, + } + + added[addedEdge] = struct{}{} + } + + return edges, nil +} + +func (g *graph[K, T]) RemoveEdge(source, target K) error { + if _, err := g.Edge(source, target); err != nil { + return err + } + + if err := g.store.RemoveEdge(source, target); err != nil { + return fmt.Errorf("failed to remove edge from %v to %v: %w", source, target, err) + } + + return nil +} + +func (g *graph[K, T]) updateEdge(source, target K, options ...func(properties *EdgeProperties)) (Edge[K], error) { + existingEdge, err := g.store.Edge(source, target) + if err != nil { + return Edge[K]{}, err + } + + for _, option := range options { + option(&existingEdge.Properties) + } + + return existingEdge, g.store.UpdateEdge(source, target, existingEdge) +} + +func (g *graph[K, T]) Traits() *Traits { + return g.traits +} + +func (g *graph[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error { + hash := g.hash(value) + properties := VertexProperties{ + Weight: 0, + Attributes: make(map[string]string), + } + + for _, option := range options { + option(&properties) + } + + return g.store.AddVertex(hash, value, properties) + +} + +func (g *graph[K, T]) addVerticesFrom(other *graph[K, T]) error { + adjacencyMap, err := other.AdjacencyMap() + if err != nil { + return fmt.Errorf("failed to get adjacency map: %w", err) + } + + for hash := range adjacencyMap { + vertex, properties, err := other.VertexWithProperties(hash) + if err != nil { + return fmt.Errorf("failed to get vertex %v: %w", hash, err) + } + + if err = g.AddVertex(vertex, copyVertexProperties(properties)); err != nil { + return fmt.Errorf("failed to add vertex %v: %w", hash, err) + } + } + + return nil +} + +func (g *graph[K, T]) Vertex(hash K) (T, error) { + vertex, _, err := g.store.Vertex(hash) + return vertex, err +} + +func (g *graph[K, T]) VertexWithProperties(hash K) (T, VertexProperties, error) { + vertex, properties, err := g.store.Vertex(hash) + if err != nil { + return vertex, VertexProperties{}, err + } + + return vertex, properties, nil +} + +func (g *graph[K, T]) RemoveVertex(hash K) error { + return g.store.RemoveVertex(hash) +} + +func (g *graph[K, T]) AdjacencyMap() (map[K]map[K]Edge[K], error) { + vertices, err := g.store.ListVertices() + if err != nil { + return nil, fmt.Errorf("failed to list vertices: %w", err) + } + + edges, err := g.store.ListEdges() + if err != nil { + return nil, fmt.Errorf("failed to list edges: %w", err) + } + + m := make(map[K]map[K]Edge[K], len(vertices)) + + for _, vertex := range vertices { + m[vertex] = make(map[K]Edge[K]) + } + + for _, edge := range edges { + m[edge.Source][edge.Target] = edge + } + + return m, nil +} + +func (g *graph[K, T]) PredecessorMap() (map[K]map[K]Edge[K], error) { + if !g.traits.IsDirected { + return g.AdjacencyMap() + } + + vertices, err := g.store.ListVertices() + if err != nil { + return nil, fmt.Errorf("failed to list vertices: %w", err) + } + + edges, err := g.store.ListEdges() + if err != nil { + return nil, fmt.Errorf("failed to list edges: %w", err) + } + + m := make(map[K]map[K]Edge[K], len(vertices)) + + for _, vertex := range vertices { + m[vertex] = make(map[K]Edge[K]) + } + + for _, edge := range edges { + if _, ok := m[edge.Target]; !ok { + m[edge.Target] = make(map[K]Edge[K]) + } + m[edge.Target][edge.Source] = edge + } + + return m, nil +} + +func (g *graph[K, T]) Order() (int, error) { + return g.store.VertexCount() +} + +func (g *graph[K, T]) Size() (int, error) { + edgeCount, err := g.store.EdgeCount() + + if g.traits.IsDirected { + return edgeCount, err + } + // Divide by 2 since every add edge operation on undirected graph is counted + // twice. + return edgeCount / 2, err +} + +func (g *graph[K, T]) createsCycle(source, target K) (bool, error) { + // If the underlying store implements CreatesCycle, use that fast path. + if cc, ok := g.store.(interface { + CreatesCycle(source, target K) (bool, error) + }); ok { + return cc.CreatesCycle(source, target) + } + + // Slow path. + return CreatesCycle[K, T](fromGraph(g), source, target) +} + +func (g *graph[K, T]) edgesAreEqual(a, b Edge[T]) bool { + aSourceHash := g.hash(a.Source) + aTargetHash := g.hash(a.Target) + bSourceHash := g.hash(b.Source) + bTargetHash := g.hash(b.Target) + + if aSourceHash == bSourceHash && aTargetHash == bTargetHash { + return true + } + + if !g.traits.IsDirected { + return aSourceHash == bTargetHash && aTargetHash == bSourceHash + } + + return false +} + +// copyEdge returns an argument list suitable for the Graph.AddEdge method. This +// argument list is derived from the given edge, hence the name copyEdge. +// +// The last argument is a custom functional option that sets the edge properties +// to the properties of the original edge. +func copyEdge[K comparable](edge Edge[K]) (K, K, func(properties *EdgeProperties)) { + copyProperties := func(p *EdgeProperties) { + for k, v := range edge.Properties.Attributes { + p.Attributes[k] = v + } + p.Weight = edge.Properties.Weight + p.Data = edge.Properties.Data + } + + return edge.Source, edge.Target, copyProperties +} + +// tograph converts a Graph interface to a graph instance. +func tograph[K comparable, T any](g Graph[K, T]) graph[K, T] { + var other graph[K, T] + if g.Traits().IsDirected { + other = g.(*directed[K, T]).graph + } else { + other = g.(*undirected[K, T]).graph + } + return other +} + +// fromGraph converts a graph instance to a Graph interface. +func fromGraph[K comparable, T any](g *graph[K, T]) Graph[K, T] { + var gr Graph[K, T] + if g.Traits().IsDirected { + gr = &directed[K, T]{graph: *g} + } else { + gr = &undirected[K, T]{graph: *g} + } + return gr +} diff --git a/undirected.go b/undirected.go index 519526e..4f3cfff 100644 --- a/undirected.go +++ b/undirected.go @@ -1,235 +1,51 @@ package graph import ( - "errors" "fmt" ) type undirected[K comparable, T any] struct { - hash Hash[K, T] - traits *Traits - store Store[K, T] + graph[K, T] } func newUndirected[K comparable, T any](hash Hash[K, T], traits *Traits, store Store[K, T]) *undirected[K, T] { return &undirected[K, T]{ - hash: hash, - traits: traits, - store: store, - } -} - -func (u *undirected[K, T]) Traits() *Traits { - return u.traits -} - -func (u *undirected[K, T]) AddVertex(value T, options ...func(*VertexProperties)) error { - hash := u.hash(value) - - prop := VertexProperties{ - Weight: 0, - Attributes: make(map[string]string), - } - - for _, option := range options { - option(&prop) - } - - return u.store.AddVertex(hash, value, prop) -} - -func (u *undirected[K, T]) Vertex(hash K) (T, error) { - vertex, _, err := u.store.Vertex(hash) - return vertex, err -} - -func (u *undirected[K, T]) VertexWithProperties(hash K) (T, VertexProperties, error) { - vertex, prop, err := u.store.Vertex(hash) - if err != nil { - return vertex, VertexProperties{}, err - } - - return vertex, prop, nil -} - -func (u *undirected[K, T]) RemoveVertex(hash K) error { - return u.store.RemoveVertex(hash) -} - -func (u *undirected[K, T]) AddEdge(sourceHash, targetHash K, options ...func(*EdgeProperties)) error { - if _, _, err := u.store.Vertex(sourceHash); err != nil { - return fmt.Errorf("could not find source vertex with hash %v: %w", sourceHash, err) - } - - if _, _, err := u.store.Vertex(targetHash); err != nil { - return fmt.Errorf("could not find target vertex with hash %v: %w", targetHash, err) - } - - //nolint:govet // False positive. - if _, err := u.Edge(sourceHash, targetHash); !errors.Is(err, ErrEdgeNotFound) { - return ErrEdgeAlreadyExists - } - - // If the user opted in to preventing cycles, run a cycle check. - if u.traits.PreventCycles { - createsCycle, err := CreatesCycle[K, T](u, sourceHash, targetHash) - if err != nil { - return fmt.Errorf("check for cycles: %w", err) - } - if createsCycle { - return ErrEdgeCreatesCycle - } - } - - edge := Edge[K]{ - Source: sourceHash, - Target: targetHash, - Properties: EdgeProperties{ - Attributes: make(map[string]string), + graph: graph[K, T]{ + hash: hash, + traits: traits, + store: store, }, } - - for _, option := range options { - option(&edge.Properties) - } - - if err := u.addEdge(sourceHash, targetHash, edge); err != nil { - return fmt.Errorf("failed to add edge: %w", err) - } - - return nil } func (u *undirected[K, T]) AddEdgesFrom(g Graph[K, T]) error { - edges, err := g.Edges() - if err != nil { - return fmt.Errorf("failed to get edges: %w", err) - } - - for _, edge := range edges { - if err := u.AddEdge(copyEdge(edge)); err != nil { - return fmt.Errorf("failed to add (%v, %v): %w", edge.Source, edge.Target, err) - } - } - - return nil + other := tograph(g) + return u.addEdgesFrom(&other) } func (u *undirected[K, T]) AddVerticesFrom(g Graph[K, T]) error { - adjacencyMap, err := g.AdjacencyMap() - if err != nil { - return fmt.Errorf("failed to get adjacency map: %w", err) - } - - for hash := range adjacencyMap { - vertex, properties, err := g.VertexWithProperties(hash) - if err != nil { - return fmt.Errorf("failed to get vertex %v: %w", hash, err) - } - - if err = u.AddVertex(vertex, copyVertexProperties(properties)); err != nil { - return fmt.Errorf("failed to add vertex %v: %w", hash, err) - } - } - - return nil + other := tograph(g) + return u.addVerticesFrom(&other) } -func (u *undirected[K, T]) Edge(sourceHash, targetHash K) (Edge[T], error) { - // In an undirected graph, since multigraphs aren't supported, the edge AB - // is the same as BA. Therefore, if source[target] cannot be found, this - // function also looks for target[source]. - edge, err := u.store.Edge(sourceHash, targetHash) - if errors.Is(err, ErrEdgeNotFound) { - edge, err = u.store.Edge(targetHash, sourceHash) - } - - if err != nil { - return Edge[T]{}, err - } - - sourceVertex, _, err := u.store.Vertex(sourceHash) +func (u *undirected[K, T]) Clone() (Graph[K, T], error) { + gclone, err := u.clone() if err != nil { - return Edge[T]{}, err + return nil, err } - targetVertex, _, err := u.store.Vertex(targetHash) - if err != nil { - return Edge[T]{}, err + dir := &undirected[K, T]{ + graph: *gclone, } - - return Edge[T]{ - Source: sourceVertex, - Target: targetVertex, - Properties: EdgeProperties{ - Weight: edge.Properties.Weight, - Attributes: edge.Properties.Attributes, - Data: edge.Properties.Data, - }, - }, nil -} - -type tuple[K comparable] struct { - source, target K -} - -func (u *undirected[K, T]) Edges() ([]Edge[K], error) { - storedEdges, err := u.store.ListEdges() - if err != nil { - return nil, fmt.Errorf("failed to get edges: %w", err) - } - - // An undirected graph creates each edge twice internally: The edge (A,B) is - // stored both as (A,B) and (B,A). The Edges method is supposed to return - // one of these two edges, because from an outside perspective, it only is - // a single edge. - // - // To achieve this, Edges keeps track of already-added edges. For each edge, - // it also checks if the reversed edge has already been added - e.g., for - // an edge (A,B), Edges checks if the edge has been added as (B,A). - // - // These reversed edges are built as a custom tuple type, which is then used - // as a map key for access in O(1) time. It looks scarier than it is. - edges := make([]Edge[K], 0, len(storedEdges)/2) - - added := make(map[tuple[K]]struct{}) - - for _, storedEdge := range storedEdges { - reversedEdge := tuple[K]{ - source: storedEdge.Target, - target: storedEdge.Source, - } - if _, ok := added[reversedEdge]; ok { - continue - } - - edges = append(edges, storedEdge) - - addedEdge := tuple[K]{ - source: storedEdge.Source, - target: storedEdge.Target, - } - - added[addedEdge] = struct{}{} - } - - return edges, nil + return dir, nil } func (u *undirected[K, T]) UpdateEdge(source, target K, options ...func(properties *EdgeProperties)) error { - existingEdge, err := u.store.Edge(source, target) + existingEdge, err := u.updateEdge(source, target, options...) if err != nil { return err } - for _, option := range options { - option(&existingEdge.Properties) - } - - if err := u.store.UpdateEdge(source, target, existingEdge); err != nil { - return err - } - reversedEdge := existingEdge reversedEdge.Source = existingEdge.Target reversedEdge.Target = existingEdge.Source @@ -238,122 +54,14 @@ func (u *undirected[K, T]) UpdateEdge(source, target K, options ...func(properti } func (u *undirected[K, T]) RemoveEdge(source, target K) error { - if _, err := u.Edge(source, target); err != nil { - return err - } - - if err := u.store.RemoveEdge(source, target); err != nil { - return fmt.Errorf("failed to remove edge from %v to %v: %w", source, target, err) - } - - if err := u.store.RemoveEdge(target, source); err != nil { - return fmt.Errorf("failed to remove edge from %v to %v: %w", target, source, err) - } - - return nil -} - -func (u *undirected[K, T]) AdjacencyMap() (map[K]map[K]Edge[K], error) { - vertices, err := u.store.ListVertices() - if err != nil { - return nil, fmt.Errorf("failed to list vertices: %w", err) - } - - edges, err := u.store.ListEdges() - if err != nil { - return nil, fmt.Errorf("failed to list edges: %w", err) - } - - m := make(map[K]map[K]Edge[K], len(vertices)) - - for _, vertex := range vertices { - m[vertex] = make(map[K]Edge[K]) - } - - for _, edge := range edges { - m[edge.Source][edge.Target] = edge - } - - return m, nil -} - -func (u *undirected[K, T]) PredecessorMap() (map[K]map[K]Edge[K], error) { - return u.AdjacencyMap() -} - -func (u *undirected[K, T]) Clone() (Graph[K, T], error) { - traits := &Traits{ - IsDirected: u.traits.IsDirected, - IsAcyclic: u.traits.IsAcyclic, - IsWeighted: u.traits.IsWeighted, - IsRooted: u.traits.IsRooted, - } - - clone := &undirected[K, T]{ - hash: u.hash, - traits: traits, - store: newMemoryStore[K, T](), - } - - if err := clone.AddVerticesFrom(u); err != nil { - return nil, fmt.Errorf("failed to add vertices: %w", err) - } - - if err := clone.AddEdgesFrom(u); err != nil { - return nil, fmt.Errorf("failed to add edges: %w", err) - } - - return clone, nil -} - -func (u *undirected[K, T]) Order() (int, error) { - return u.store.VertexCount() -} - -func (u *undirected[K, T]) Size() (int, error) { - edgeCount, err := u.store.EdgeCount() - - // Divide by 2 since every add edge operation on undirected graph is counted - // twice. - return edgeCount / 2, err -} - -func (u *undirected[K, T]) edgesAreEqual(a, b Edge[T]) bool { - aSourceHash := u.hash(a.Source) - aTargetHash := u.hash(a.Target) - bSourceHash := u.hash(b.Source) - bTargetHash := u.hash(b.Target) - - if aSourceHash == bSourceHash && aTargetHash == bTargetHash { - return true - } - - if !u.traits.IsDirected { - return aSourceHash == bTargetHash && aTargetHash == bSourceHash - } - - return false -} - -func (u *undirected[K, T]) addEdge(sourceHash, targetHash K, edge Edge[K]) error { - err := u.store.AddEdge(sourceHash, targetHash, edge) + err := u.graph.RemoveEdge(source, target) if err != nil { return err } - rEdge := Edge[K]{ - Source: edge.Target, - Target: edge.Source, - Properties: EdgeProperties{ - Weight: edge.Properties.Weight, - Attributes: edge.Properties.Attributes, - Data: edge.Properties.Data, - }, - } - - err = u.store.AddEdge(targetHash, sourceHash, rEdge) - if err != nil { - return err + // remove reciprocal edge + if err = u.store.RemoveEdge(target, source); err != nil { + return fmt.Errorf("failed to remove edge from %v to %v: %w", target, source, err) } return nil