From b51935fcd1398a68ad8af1f23ab1147c069e432f Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Tue, 21 Aug 2018 17:29:01 -0400 Subject: [PATCH] graph: implement Join/Disjoin --- graph/graph.go | 89 +++++++++++++++++++++++++++++++++++++++++++-- graph/graph_test.go | 79 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 3 deletions(-) diff --git a/graph/graph.go b/graph/graph.go index 2f22491..ec7b415 100644 --- a/graph/graph.go +++ b/graph/graph.go @@ -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 { diff --git a/graph/graph_test.go b/graph/graph_test.go index d9da5a9..81ebab1 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -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) + } +}