parent
c277bab368
commit
132a50039b
@ -0,0 +1,156 @@ |
||||
// Package graph implements an immutable unidirectional graph.
|
||||
package graph |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"encoding/hex" |
||||
"fmt" |
||||
) |
||||
|
||||
// Value wraps a go value in a way such that it will be uniquely identified
|
||||
// within any Graph and between Graphs. Use NewValue to create a Value instance.
|
||||
// You can create an instance manually as long as ID is globally unique.
|
||||
type Value struct { |
||||
ID string |
||||
V interface{} |
||||
} |
||||
|
||||
// Void is the absence of any value.
|
||||
var Void Value |
||||
|
||||
// NewValue returns a Value instance wrapping any go value. The Value returned
|
||||
// will be independent of the passed in go value. So if the same go value is
|
||||
// passed in twice then the two returned Value instances will be treated as
|
||||
// being different values by Graph.
|
||||
func NewValue(V interface{}) Value { |
||||
b := make([]byte, 8) |
||||
if _, err := rand.Read(b); err != nil { |
||||
panic(err) |
||||
} |
||||
return Value{ |
||||
ID: hex.EncodeToString(b), |
||||
V: V, |
||||
} |
||||
} |
||||
|
||||
// Edge is a directional edge connecting two values in a Graph, the Tail and the
|
||||
// Head. An Edge may also contain a value of its own.
|
||||
type Edge struct { |
||||
Tail, Val, Head Value |
||||
} |
||||
|
||||
func (e Edge) id() string { |
||||
return fmt.Sprintf("%q-%q->%q", e.Tail, e.Val, e.Head) |
||||
} |
||||
|
||||
// Graph implements an immutable, unidirectional graph which can hold generic
|
||||
// values. All methods are thread-safe as they don't modify the Graph in any
|
||||
// way.
|
||||
//
|
||||
// The Graph's zero value is the initial empty graph.
|
||||
//
|
||||
// The Graph does not keep track of Edge ordering. Assume that all slices of
|
||||
// Edges are in random order.
|
||||
type Graph struct { |
||||
m map[string]Edge |
||||
} |
||||
|
||||
func (g Graph) cp() Graph { |
||||
g2 := Graph{ |
||||
m: make(map[string]Edge, len(g.m)), |
||||
} |
||||
for id, e := range g.m { |
||||
g2.m[id] = e |
||||
} |
||||
return g2 |
||||
} |
||||
|
||||
// AddEdge 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) AddEdge(e Edge) Graph { |
||||
id := e.id() |
||||
if _, ok := g.m[id]; ok { |
||||
return g |
||||
} |
||||
|
||||
g2 := g.cp() |
||||
g2.m[id] = e |
||||
return g2 |
||||
} |
||||
|
||||
// DelEdge 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) DelEdge(e Edge) Graph { |
||||
id := e.id() |
||||
if _, ok := g.m[id]; !ok { |
||||
return g |
||||
} |
||||
|
||||
g2 := g.cp() |
||||
delete(g2.m, id) |
||||
return g2 |
||||
} |
||||
|
||||
// Values returns all Values which have incoming or outgoing Edges in the Graph.
|
||||
func (g Graph) Values() []Value { |
||||
values := make([]Value, 0, len(g.m)) |
||||
found := map[string]bool{} |
||||
tryAdd := func(v Value) { |
||||
if ok := found[v.ID]; !ok { |
||||
values = append(values, v) |
||||
found[v.ID] = true |
||||
} |
||||
} |
||||
|
||||
for _, e := range g.m { |
||||
tryAdd(e.Head) |
||||
tryAdd(e.Tail) |
||||
} |
||||
return values |
||||
} |
||||
|
||||
// Edges returns all Edges which are part of the Graph
|
||||
func (g Graph) Edges() []Edge { |
||||
edges := make([]Edge, 0, len(g.m)) |
||||
for _, e := range g.m { |
||||
edges = append(edges, e) |
||||
} |
||||
return edges |
||||
} |
||||
|
||||
// ValueEdges returns all input (e.Head==v) and output (e.Tail==v) Edges
|
||||
// for the given Value in the Graph.
|
||||
func (g Graph) ValueEdges(v Value) ([]Edge, []Edge) { |
||||
var in, out []Edge |
||||
for _, e := range g.m { |
||||
if e.Tail.ID == v.ID { |
||||
out = append(out, e) |
||||
} |
||||
if e.Head.ID == v.ID { |
||||
in = append(in, e) |
||||
} |
||||
} |
||||
return in, out |
||||
} |
||||
|
||||
// Traverse is used to traverse the Graph until a stopping point is reached.
|
||||
// Traversal starts with the cursor at the given start value. Each hop is
|
||||
// performed by passing the cursor value along with its input and output Edges
|
||||
// into the next function. The cursor moves to the returned Value and next is
|
||||
// called again, and so on.
|
||||
//
|
||||
// If the boolean returned from the next function is false traversal stops and
|
||||
// this method returns.
|
||||
//
|
||||
// If start has no Edges in the Graph, or a Value returned from next doesn't,
|
||||
// this will still call next, but the in/out params will both be empty.
|
||||
func (g Graph) Traverse(start Value, next func(v Value, in, out []Edge) (Value, bool)) { |
||||
curr := start |
||||
var ok bool |
||||
for { |
||||
in, out := g.ValueEdges(curr) |
||||
if curr, ok = next(curr, in, out); !ok { |
||||
return |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,135 @@ |
||||
package graph |
||||
|
||||
import ( |
||||
. "testing" |
||||
"time" |
||||
|
||||
"github.com/mediocregopher/mediocre-go-lib/mrand" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" |
||||
"github.com/mediocregopher/mediocre-go-lib/mtest/mchk" |
||||
) |
||||
|
||||
func TestGraph(t *T) { |
||||
type state struct { |
||||
Graph |
||||
|
||||
m map[string]Edge |
||||
} |
||||
|
||||
type params struct { |
||||
add Edge |
||||
del Edge |
||||
} |
||||
|
||||
strV := func(s string) Value { |
||||
return Value{ID: s, V: s} |
||||
} |
||||
|
||||
chk := mchk.Checker{ |
||||
Init: func() mchk.State { |
||||
return state{ |
||||
m: map[string]Edge{}, |
||||
} |
||||
}, |
||||
Next: func(ss mchk.State) mchk.Action { |
||||
s := ss.(state) |
||||
var p params |
||||
if i := mrand.Intn(10); i == 0 { |
||||
// add edge which is already there
|
||||
for _, e := range s.m { |
||||
p.add = e |
||||
break |
||||
} |
||||
} else if i == 1 { |
||||
// delete edge which isn't there
|
||||
p.del = Edge{Tail: strV("z"), Val: strV("z"), Head: strV("z")} |
||||
} else if i <= 5 { |
||||
// add probably new edge
|
||||
p.add = Edge{ |
||||
Tail: strV(mrand.Hex(1)), |
||||
Val: strV(mrand.Hex(1)), |
||||
Head: strV(mrand.Hex(1)), |
||||
} |
||||
} else { |
||||
// probably del edge
|
||||
p.del = Edge{ |
||||
Tail: strV(mrand.Hex(1)), |
||||
Val: strV(mrand.Hex(1)), |
||||
Head: strV(mrand.Hex(1)), |
||||
} |
||||
} |
||||
return mchk.Action{Params: p} |
||||
}, |
||||
Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { |
||||
s, p := ss.(state), a.Params.(params) |
||||
if p.add != (Edge{}) { |
||||
s.Graph = s.Graph.AddEdge(p.add) |
||||
s.m[p.add.id()] = p.add |
||||
} else { |
||||
s.Graph = s.Graph.DelEdge(p.del) |
||||
delete(s.m, p.del.id()) |
||||
} |
||||
|
||||
{ // test Values and Edges methods
|
||||
vals := s.Graph.Values() |
||||
edges := s.Graph.Edges() |
||||
var aa []massert.Assertion |
||||
found := map[string]bool{} |
||||
tryAssert := func(v Value) { |
||||
if ok := found[v.ID]; !ok { |
||||
found[v.ID] = true |
||||
aa = append(aa, massert.Has(vals, v)) |
||||
} |
||||
} |
||||
for _, e := range s.m { |
||||
aa = append(aa, massert.Has(edges, e)) |
||||
tryAssert(e.Head) |
||||
tryAssert(e.Tail) |
||||
} |
||||
aa = append(aa, massert.Len(vals, len(found))) |
||||
aa = append(aa, massert.Len(edges, len(s.m))) |
||||
if err := massert.All(aa...).Assert(); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
{ // test ValueEdges
|
||||
for _, val := range s.Graph.Values() { |
||||
in, out := s.Graph.ValueEdges(val) |
||||
var expIn, expOut []Edge |
||||
for _, e := range s.m { |
||||
if e.Tail.ID == val.ID { |
||||
expOut = append(expOut, e) |
||||
} |
||||
if e.Head.ID == val.ID { |
||||
expIn = append(expIn, e) |
||||
} |
||||
} |
||||
if err := massert.Comment(massert.All( |
||||
massert.Subset(expIn, in), |
||||
massert.Len(in, len(expIn)), |
||||
massert.Subset(expOut, out), |
||||
massert.Len(out, len(expOut)), |
||||
), "val:%q", val.V).Assert(); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
|
||||
return s, nil |
||||
}, |
||||
MaxLength: 10, |
||||
} |
||||
|
||||
err := chk.RunCase( |
||||
params{add: Edge{Tail: strV("4"), Val: strV("d"), Head: strV("4")}}, |
||||
params{del: Edge{Tail: strV("4"), Val: strV("d"), Head: strV("4")}}, |
||||
) |
||||
if err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
|
||||
if err := chk.RunFor(5 * time.Second); err != nil { |
||||
t.Fatal(err) |
||||
} |
||||
} |
Loading…
Reference in new issue