graph: implement Join/Disjoin

This commit is contained in:
Brian Picciano 2018-08-21 17:29:01 -04:00
parent 20b2a80a3c
commit b51935fcd1
2 changed files with 165 additions and 3 deletions

View File

@ -5,6 +5,8 @@ import (
"crypto/rand"
"encoding/hex"
"fmt"
"sort"
"strings"
)
// Value wraps a go value in a way such that it will be uniquely identified
@ -111,6 +113,15 @@ func (g Graph) cp() Graph {
return g2
}
func (g Graph) String() string {
edgeIDs := make([]string, 0, len(g.m))
for edgeID := range g.m {
edgeIDs = append(edgeIDs, edgeID)
}
sort.Strings(edgeIDs)
return "Graph{" + strings.Join(edgeIDs, ",") + "}"
}
// Add returns a new Graph instance with the given Edge added to it. If the
// original Graph already had that Edge this returns the original Graph.
func (g Graph) Add(e Edge) Graph {
@ -120,12 +131,16 @@ func (g Graph) Add(e Edge) Graph {
}
g2 := g.cp()
g2.m[id] = e
g2.vIns.add(e.Head.ID, id)
g2.vOuts.add(e.Tail.ID, id)
g2.addDirty(id, e)
return g2
}
func (g Graph) addDirty(edgeID string, e Edge) {
g.m[edgeID] = e
g.vIns.add(e.Head.ID, edgeID)
g.vOuts.add(e.Tail.ID, edgeID)
}
// Del returns a new Graph instance without the given Edge in it. If the
// original Graph didn't have that Edge this returns the original Graph.
func (g Graph) Del(e Edge) Graph {
@ -141,6 +156,72 @@ func (g Graph) Del(e Edge) Graph {
return g2
}
// Disjoin looks at the whole Graph and returns all sub-graphs of it which don't
// share any Edges between each other.
func (g Graph) Disjoin() []Graph {
valM := make(map[string]*Graph, len(g.vOuts))
graphForEdge := func(edge Edge) *Graph {
headGraph := valM[edge.Head.ID]
tailGraph := valM[edge.Tail.ID]
if headGraph == nil && tailGraph == nil {
newGraph := Graph{}.cp() // cp also initializes
return &newGraph
} else if headGraph == nil && tailGraph != nil {
return tailGraph
} else if headGraph != nil && tailGraph == nil {
return headGraph
} else if headGraph == tailGraph {
return headGraph // doesn't matter which is returned
}
// the two values are part of different graphs, join the smaller into
// the larger and change all values which were pointing to it to point
// into the larger (which will then be the join of them)
if len(tailGraph.m) > len(headGraph.m) {
tailGraph, headGraph = headGraph, tailGraph
}
for edgeID, edge := range tailGraph.m {
(*headGraph).addDirty(edgeID, edge)
}
for valID, valGraph := range valM {
if valGraph == tailGraph {
valM[valID] = headGraph
}
}
return headGraph
}
for edgeID, edge := range g.m {
graph := graphForEdge(edge)
(*graph).addDirty(edgeID, edge)
valM[edge.Head.ID] = graph
valM[edge.Tail.ID] = graph
}
found := map[*Graph]bool{}
graphs := make([]Graph, 0, len(valM))
for _, graph := range valM {
if found[graph] {
continue
}
found[graph] = true
graphs = append(graphs, *graph)
}
return graphs
}
// Join returns a new Graph which shares all Edges of this Graph and all given
// Graphs.
func (g Graph) Join(graphs ...Graph) Graph {
g2 := g.cp()
for _, graph := range graphs {
for edgeID, edge := range graph.m {
g2.addDirty(edgeID, edge)
}
}
return g2
}
// Edges returns all Edges which are part of the Graph
func (g Graph) Edges() []Edge {
edges := make([]Edge, 0, len(g.m))
@ -237,6 +318,8 @@ func (g Graph) Traverse(start Value, next func(n Node) (Value, bool)) {
}
}
// TODO VisitBreadth/VisitDepth
func (g Graph) edgesShared(g2 Graph) bool {
for id := range g2.m {
if _, ok := g.m[id]; !ok {

View File

@ -179,3 +179,82 @@ func TestSubGraphAndEqual(t *T) {
t.Fatal(err)
}
}
func TestDisjoinUnion(t *T) {
t.Parallel()
type state struct {
g Graph
// prefix -> Values with that prefix. contains dupes
valM map[string][]Value
disjM map[string]Graph
}
type params struct {
prefix string
e Edge
}
chk := mchk.Checker{
Init: func() mchk.State {
return state{
valM: map[string][]Value{},
disjM: map[string]Graph{},
}
},
Next: func(ss mchk.State) mchk.Action {
s := ss.(state)
prefix := mrand.Hex(1)
var edge Edge
if vals := s.valM[prefix]; len(vals) == 0 {
edge = Edge{
Tail: strV(prefix + mrand.Hex(1)),
Head: strV(prefix + mrand.Hex(1)),
}
} else if mrand.Intn(2) == 0 {
edge = Edge{
Tail: mrand.Element(vals, nil).(Value),
Head: strV(prefix + mrand.Hex(1)),
}
} else {
edge = Edge{
Tail: strV(prefix + mrand.Hex(1)),
Head: mrand.Element(vals, nil).(Value),
}
}
return mchk.Action{Params: params{prefix: prefix, e: edge}}
},
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) {
s, p := ss.(state), a.Params.(params)
s.g = s.g.Add(p.e)
s.valM[p.prefix] = append(s.valM[p.prefix], p.e.Head, p.e.Tail)
s.disjM[p.prefix] = s.disjM[p.prefix].Add(p.e)
var aa []massert.Assertion
// test Disjoin
disj := s.g.Disjoin()
for prefix, graph := range s.disjM {
aa = append(aa, massert.Comment(
massert.Equal(true, graph.Equal(s.disjM[prefix])),
"prefix:%q", prefix,
))
}
aa = append(aa, massert.Len(disj, len(s.disjM)))
// now test Join
join := (Graph{}).Join(disj...)
aa = append(aa, massert.Equal(true, s.g.Equal(join)))
return s, massert.All(aa...).Assert()
},
MaxLength: 100,
// Each action is required for subsequent ones to make sense, so
// minimizing won't work
DontMinimize: true,
}
if err := chk.RunFor(5 * time.Second); err != nil {
t.Fatal(err)
}
}