diff --git a/graph/encoding/dot/decode.go b/graph/encoding/dot/decode.go index c410b888..26f03272 100644 --- a/graph/encoding/dot/decode.go +++ b/graph/encoding/dot/decode.go @@ -50,6 +50,19 @@ func Unmarshal(data []byte, dst encoding.Builder) error { return copyGraph(dst, file.Graphs[0]) } +// UnmarshalMulti parses the Graphviz DOT-encoded data as a multigraph and +// stores the result in dst. +func UnmarshalMulti(data []byte, dst encoding.MultiBuilder) error { + file, err := dot.ParseBytes(data) + if err != nil { + return err + } + if len(file.Graphs) != 1 { + return fmt.Errorf("invalid number of graphs; expected 1, got %d", len(file.Graphs)) + } + return copyMultigraph(dst, file.Graphs[0]) +} + // copyGraph copies the nodes and edges from the Graphviz AST source graph to // the destination graph. Edge direction is maintained if present. func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) { @@ -62,9 +75,41 @@ func copyGraph(dst encoding.Builder, src *ast.Graph) (err error) { panic(e) } }() - gen := &generator{ - directed: src.Directed, - ids: make(map[string]graph.Node), + gen := &simpleGraph{ + generator: generator{ + directed: src.Directed, + ids: make(map[string]graph.Node), + }, + } + if dst, ok := dst.(DOTIDSetter); ok { + dst.SetDOTID(src.ID) + } + if a, ok := dst.(AttributeSetters); ok { + gen.graphAttr, gen.nodeAttr, gen.edgeAttr = a.DOTAttributeSetters() + } + for _, stmt := range src.Stmts { + gen.addStmt(dst, stmt) + } + return err +} + +// copyMultigraph copies the nodes and edges from the Graphviz AST source graph to +// the destination graph. Edge direction is maintained if present. +func copyMultigraph(dst encoding.MultiBuilder, src *ast.Graph) (err error) { + defer func() { + switch e := recover().(type) { + case nil: + case error: + err = e + default: + panic(e) + } + }() + gen := &multiGraph{ + generator: generator{ + directed: src.Directed, + ids: make(map[string]graph.Node), + }, } if dst, ok := dst.(DOTIDSetter); ok { dst.SetDOTID(src.ID) @@ -97,7 +142,7 @@ type generator struct { // node returns the gonum node corresponding to the given dot AST node ID, // generating a new such node if none exist. -func (gen *generator) node(dst encoding.Builder, id string) graph.Node { +func (gen *generator) node(dst graph.NodeAdder, id string) graph.Node { if n, ok := gen.ids[id]; ok { return n } @@ -117,8 +162,10 @@ func (gen *generator) node(dst encoding.Builder, id string) graph.Node { return n } +type simpleGraph struct{ generator } + // addStmt adds the given statement to the graph. -func (gen *generator) addStmt(dst encoding.Builder, stmt ast.Stmt) { +func (gen *simpleGraph) addStmt(dst encoding.Builder, stmt ast.Stmt) { switch stmt := stmt.(type) { case *ast.NodeStmt: n, ok := gen.node(dst, stmt.Node.ID).(encoding.AttributeSetter) @@ -206,7 +253,7 @@ func applyPortsToEdge(from ast.Vertex, to *ast.Edge, edge graph.Edge) { } // addEdgeStmt adds the given edge statement to the graph. -func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { +func (gen *simpleGraph) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { fs := gen.addVertex(dst, stmt.From) ts := gen.addEdge(dst, stmt.To, stmt.Attrs) for _, f := range fs { @@ -220,7 +267,7 @@ func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { } // addVertex adds the given vertex to the graph, and returns its set of nodes. -func (gen *generator) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node { +func (gen *simpleGraph) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node { switch v := v.(type) { case *ast.Node: n := gen.node(dst, v.ID) @@ -237,7 +284,7 @@ func (gen *generator) addVertex(dst encoding.Builder, v ast.Vertex) []graph.Node } // addEdge adds the given edge to the graph, and returns its set of nodes. -func (gen *generator) addEdge(dst encoding.Builder, to *ast.Edge, attrs []*ast.Attr) []graph.Node { +func (gen *simpleGraph) addEdge(dst encoding.Builder, to *ast.Edge, attrs []*ast.Attr) []graph.Node { if !gen.directed && to.Directed { panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex)) } @@ -307,6 +354,123 @@ func (gen *generator) appendSubgraphNode(n graph.Node) { gen.subNodes = append(gen.subNodes, n) } +type multiGraph struct{ generator } + +// addStmt adds the given statement to the multigraph. +func (gen *multiGraph) addStmt(dst encoding.MultiBuilder, stmt ast.Stmt) { + switch stmt := stmt.(type) { + case *ast.NodeStmt: + n, ok := gen.node(dst, stmt.Node.ID).(encoding.AttributeSetter) + if !ok { + return + } + for _, attr := range stmt.Attrs { + a := encoding.Attribute{ + Key: attr.Key, + Value: attr.Val, + } + if err := n.SetAttribute(a); err != nil { + panic(fmt.Errorf("unable to unmarshal node DOT attribute (%s=%s)", a.Key, a.Value)) + } + } + case *ast.EdgeStmt: + gen.addEdgeStmt(dst, stmt) + case *ast.AttrStmt: + var n encoding.AttributeSetter + var dst string + switch stmt.Kind { + case ast.GraphKind: + if gen.graphAttr == nil { + return + } + n = gen.graphAttr + dst = "graph" + case ast.NodeKind: + if gen.nodeAttr == nil { + return + } + n = gen.nodeAttr + dst = "node" + case ast.EdgeKind: + if gen.edgeAttr == nil { + return + } + n = gen.edgeAttr + dst = "edge" + default: + panic("unreachable") + } + for _, attr := range stmt.Attrs { + a := encoding.Attribute{ + Key: attr.Key, + Value: attr.Val, + } + if err := n.SetAttribute(a); err != nil { + panic(fmt.Errorf("unable to unmarshal global %s DOT attribute (%s=%s)", dst, a.Key, a.Value)) + } + } + case *ast.Attr: + // ignore. + case *ast.Subgraph: + for _, stmt := range stmt.Stmts { + gen.addStmt(dst, stmt) + } + default: + panic(fmt.Sprintf("unknown statement type %T", stmt)) + } +} + +// addEdgeStmt adds the given edge statement to the multigraph. +func (gen *multiGraph) addEdgeStmt(dst encoding.MultiBuilder, stmt *ast.EdgeStmt) { + fs := gen.addVertex(dst, stmt.From) + ts := gen.addLine(dst, stmt.To, stmt.Attrs) + for _, f := range fs { + for _, t := range ts { + edge := dst.NewLine(f, t) + dst.SetLine(edge) + applyPortsToEdge(stmt.From, stmt.To, edge) + addEdgeAttrs(edge, stmt.Attrs) + } + } +} + +// addVertex adds the given vertex to the multigraph, and returns its set of nodes. +func (gen *multiGraph) addVertex(dst encoding.MultiBuilder, v ast.Vertex) []graph.Node { + switch v := v.(type) { + case *ast.Node: + n := gen.node(dst, v.ID) + return []graph.Node{n} + case *ast.Subgraph: + gen.pushSubgraph() + for _, stmt := range v.Stmts { + gen.addStmt(dst, stmt) + } + return gen.popSubgraph() + default: + panic(fmt.Sprintf("unknown vertex type %T", v)) + } +} + +// addLine adds the given edge to the multigraph, and returns its set of nodes. +func (gen *multiGraph) addLine(dst encoding.MultiBuilder, to *ast.Edge, attrs []*ast.Attr) []graph.Node { + if !gen.directed && to.Directed { + panic(fmt.Errorf("directed edge to %v in undirected graph", to.Vertex)) + } + fs := gen.addVertex(dst, to.Vertex) + if to.To != nil { + ts := gen.addLine(dst, to.To, attrs) + for _, f := range fs { + for _, t := range ts { + edge := dst.NewLine(f, t) + dst.SetLine(edge) + applyPortsToEdge(to.Vertex, to.To, edge) + addEdgeAttrs(edge, attrs) + } + } + } + return fs +} + // addEdgeAttrs adds the attributes to the given edge. func addEdgeAttrs(edge graph.Edge, attrs []*ast.Attr) { e, ok := edge.(encoding.AttributeSetter) diff --git a/graph/encoding/dot/decode_test.go b/graph/encoding/dot/decode_test.go index 977bde1f..20fb557e 100644 --- a/graph/encoding/dot/decode_test.go +++ b/graph/encoding/dot/decode_test.go @@ -10,6 +10,7 @@ import ( "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/encoding" + "gonum.org/v1/gonum/graph/multi" "gonum.org/v1/gonum/graph/simple" ) @@ -293,6 +294,107 @@ const undirectedNonchained = `strict graph { B -- C [label="baz 2"]; }` +func TestMultigraphDecoding(t *testing.T) { + for i, test := range []struct { + directed bool + input string + expected string + }{ + { + directed: true, + input: directedMultigraph, + expected: directedMultigraph, + }, + { + directed: false, + input: undirectedMultigraph, + expected: undirectedMultigraph, + }, + { + directed: true, + input: directedSelfLoopMultigraph, + expected: directedSelfLoopMultigraph, + }, + { + directed: false, + input: undirectedSelfLoopMultigraph, + expected: undirectedSelfLoopMultigraph, + }, + } { + var dst encoding.MultiBuilder + if test.directed { + dst = multi.NewDirectedGraph() + } else { + dst = multi.NewUndirectedGraph() + } + + if err := UnmarshalMulti([]byte(test.input), dst); err != nil { + t.Errorf("i=%d: unable to unmarshal DOT graph; %v", i, err) + continue + } + buf, err := MarshalMulti(dst, "", "", "\t") + if err != nil { + t.Errorf("i=%d: unable to marshal graph; %v", i, dst) + continue + } + actual := string(buf) + if actual != test.expected { + t.Errorf("i=%d: graph content mismatch; want:\n%s\n\nactual:\n%s", i, test.expected, actual) + continue + } + } +} + +const directedMultigraph = `digraph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -> 1; + 0 -> 1; + 0 -> 2; + 2 -> 0; +}` + +const undirectedMultigraph = `graph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -- 1; + 0 -- 1; + 0 -- 2; + 0 -- 2; +}` + +const directedSelfLoopMultigraph = `digraph { + // Node definitions. + 0; + 1; + + // Edge definitions. + 0 -> 0; + 0 -> 0; + 1 -> 1; + 1 -> 1; +}` + +const undirectedSelfLoopMultigraph = `graph { + // Node definitions. + 0; + 1; + + // Edge definitions. + 0 -- 0; + 0 -- 0; + 1 -- 1; + 1 -- 1; +}` + // Below follows a minimal implementation of a graph capable of validating the // round-trip encoding and decoding of DOT graphs with nodes and edges // containing DOT attributes. diff --git a/graph/encoding/dot/encode.go b/graph/encoding/dot/encode.go index f8d2c1bd..7a919047 100644 --- a/graph/encoding/dot/encode.go +++ b/graph/encoding/dot/encode.go @@ -55,17 +55,33 @@ type Structurer interface { Structure() []Graph } +// MultiStructurer represents a graph.Multigraph that can define subgraphs. +type MultiStructurer interface { + Structure() []Multigraph +} + // Graph wraps named graph.Graph values. type Graph interface { graph.Graph DOTID() string } +// Multigraph wraps named graph.Multigraph values. +type Multigraph interface { + graph.Multigraph + DOTID() string +} + // Subgrapher wraps graph.Node values that represent subgraphs. type Subgrapher interface { Subgraph() graph.Graph } +// MultiSubgrapher wraps graph.Node values that represent subgraphs. +type MultiSubgrapher interface { + Subgraph() graph.Multigraph +} + // Marshal returns the DOT encoding for the graph g, applying the prefix // and indent to the encoding. Name is used to specify the graph name. If // name is empty and g implements Graph, the returned string from DOTID @@ -76,7 +92,7 @@ type Subgrapher interface { // implementation of the Node, Attributer, Porter, Attributers, Structurer, // Subgrapher and Graph interfaces. func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) { - var p printer + var p simpleGraphPrinter p.indent = indent p.prefix = prefix p.visited = make(map[edge]bool) @@ -87,6 +103,28 @@ func Marshal(g graph.Graph, name, prefix, indent string) ([]byte, error) { return p.buf.Bytes(), nil } +// MarshalMulti returns the DOT encoding for the multigraph g, applying the +// prefix and indent to the encoding. Name is used to specify the graph name. If +// name is empty and g implements Graph, the returned string from DOTID +// will be used. If strict is true the output bytes will be prefixed with +// the DOT "strict" keyword. +// +// Graph serialization will work for a graph.Multigraph without modification, +// however, advanced GraphViz DOT features provided by Marshal depend on +// implementation of the Node, Attributer, Porter, Attributers, Structurer, +// MultiSubgrapher and Multigraph interfaces. +func MarshalMulti(g graph.Multigraph, name, prefix, indent string) ([]byte, error) { + var p multiGraphPrinter + p.indent = indent + p.prefix = prefix + p.visited = make(map[line]bool) + err := p.print(g, name, false, false) + if err != nil { + return nil, err + } + return p.buf.Bytes(), nil +} + type printer struct { buf bytes.Buffer @@ -94,8 +132,6 @@ type printer struct { indent string depth int - visited map[edge]bool - err error } @@ -104,38 +140,17 @@ type edge struct { from, to int64 } -func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error { - nodes := graph.NodesOf(g.Nodes()) - sort.Sort(ordered.ByID(nodes)) - - p.buf.WriteString(p.prefix) - if needsIndent { - for i := 0; i < p.depth; i++ { - p.buf.WriteString(p.indent) - } - } - if !isSubgraph { - p.buf.WriteString("strict ") - } - _, isDirected := g.(graph.Directed) - if isSubgraph { - p.buf.WriteString("sub") - } else if isDirected { - p.buf.WriteString("di") - } - p.buf.WriteString("graph") +func (p *simpleGraphPrinter) print(g graph.Graph, name string, needsIndent, isSubgraph bool) error { if name == "" { if g, ok := g.(Graph); ok { name = g.DOTID() } } - if name != "" { - p.buf.WriteByte(' ') - p.buf.WriteString(name) - } - p.openBlock(" {") + _, isDirected := g.(graph.Directed) + p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, true) + if a, ok := g.(Attributers); ok { p.writeAttributeComplex(a) } @@ -150,6 +165,9 @@ func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool } } + nodes := graph.NodesOf(g.Nodes()) + sort.Sort(ordered.ByID(nodes)) + havePrintedNodeHeader := false for _, n := range nodes { if s, ok := n.(Subgrapher); ok { @@ -264,11 +282,40 @@ func (p *printer) print(g graph.Graph, name string, needsIndent, isSubgraph bool p.buf.WriteByte(';') } } + p.closeBlock("}") return nil } +func (p *printer) printFrontMatter(name string, needsIndent, isSubgraph, isDirected, isStrict bool) error { + p.buf.WriteString(p.prefix) + if needsIndent { + for i := 0; i < p.depth; i++ { + p.buf.WriteString(p.indent) + } + } + + if !isSubgraph && isStrict { + p.buf.WriteString("strict ") + } + + if isSubgraph { + p.buf.WriteString("sub") + } else if isDirected { + p.buf.WriteString("di") + } + p.buf.WriteString("graph") + + if name != "" { + p.buf.WriteByte(' ') + p.buf.WriteString(name) + } + + p.openBlock(" {") + return nil +} + func (p *printer) writeNode(n graph.Node) { p.buf.WriteString(nodeID(n)) } @@ -293,7 +340,7 @@ func nodeID(n graph.Node) string { } } -func graphID(g graph.Graph, n graph.Node) string { +func graphID(g interface{}, n graph.Node) string { switch g := g.(type) { case Node: return g.DOTID() @@ -372,3 +419,165 @@ func (p *printer) closeBlock(b string) { p.newline() p.buf.WriteString(b) } + +type simpleGraphPrinter struct { + printer + visited map[edge]bool +} + +type multiGraphPrinter struct { + printer + visited map[line]bool +} + +type line struct { + inGraph string + id int64 +} + +func (p *multiGraphPrinter) print(g graph.Multigraph, name string, needsIndent, isSubgraph bool) error { + if name == "" { + if g, ok := g.(Multigraph); ok { + name = g.DOTID() + } + } + + _, isDirected := g.(graph.Directed) + p.printFrontMatter(name, needsIndent, isSubgraph, isDirected, false) + + if a, ok := g.(Attributers); ok { + p.writeAttributeComplex(a) + } + if s, ok := g.(MultiStructurer); ok { + for _, g := range s.Structure() { + _, subIsDirected := g.(graph.Directed) + if subIsDirected != isDirected { + return errors.New("dot: mismatched graph type") + } + p.buf.WriteByte('\n') + p.print(g, g.DOTID(), true, true) + } + } + + nodes := graph.NodesOf(g.Nodes()) + sort.Sort(ordered.ByID(nodes)) + + havePrintedNodeHeader := false + for _, n := range nodes { + if s, ok := n.(MultiSubgrapher); ok { + // If the node is not linked to any other node + // the graph needs to be written now. + if g.From(n.ID()).Len() == 0 { + g := s.Subgraph() + _, subIsDirected := g.(graph.Directed) + if subIsDirected != isDirected { + return errors.New("dot: mismatched graph type") + } + if !havePrintedNodeHeader { + p.newline() + p.buf.WriteString("// Node definitions.") + havePrintedNodeHeader = true + } + p.newline() + p.print(g, graphID(g, n), false, true) + } + continue + } + if !havePrintedNodeHeader { + p.newline() + p.buf.WriteString("// Node definitions.") + havePrintedNodeHeader = true + } + p.newline() + p.writeNode(n) + if a, ok := n.(encoding.Attributer); ok { + p.writeAttributeList(a) + } + p.buf.WriteByte(';') + } + + havePrintedEdgeHeader := false + for _, n := range nodes { + nid := n.ID() + to := graph.NodesOf(g.From(nid)) + sort.Sort(ordered.ByID(to)) + + for _, t := range to { + tid := t.ID() + + lines := graph.LinesOf(g.Lines(nid, tid)) + sort.Sort(ordered.LinesByIDs(lines)) + + for _, l := range lines { + lid := l.ID() + if p.visited[line{inGraph: name, id: lid}] { + continue + } + p.visited[line{inGraph: name, id: lid}] = true + + if !havePrintedEdgeHeader { + p.buf.WriteByte('\n') + p.buf.WriteString(strings.TrimRight(p.prefix, " \t\n")) // Trim whitespace suffix. + p.newline() + p.buf.WriteString("// Edge definitions.") + havePrintedEdgeHeader = true + } + p.newline() + + if s, ok := n.(MultiSubgrapher); ok { + g := s.Subgraph() + _, subIsDirected := g.(graph.Directed) + if subIsDirected != isDirected { + return errors.New("dot: mismatched graph type") + } + p.print(g, graphID(g, n), false, true) + } else { + p.writeNode(n) + } + + porter, edgeIsPorter := l.(Porter) + if edgeIsPorter { + if l.From().ID() == nid { + p.writePorts(porter.FromPort()) + } else { + p.writePorts(porter.ToPort()) + } + } + + if isDirected { + p.buf.WriteString(" -> ") + } else { + p.buf.WriteString(" -- ") + } + + if s, ok := t.(MultiSubgrapher); ok { + g := s.Subgraph() + _, subIsDirected := g.(graph.Directed) + if subIsDirected != isDirected { + return errors.New("dot: mismatched graph type") + } + p.print(g, graphID(g, t), false, true) + } else { + p.writeNode(t) + } + if edgeIsPorter { + if l.From().ID() == nid { + p.writePorts(porter.ToPort()) + } else { + p.writePorts(porter.FromPort()) + } + } + + if a, ok := l.(encoding.Attributer); ok { + p.writeAttributeList(a) + } + + p.buf.WriteByte(';') + } + } + } + + p.closeBlock("}") + + return nil +} diff --git a/graph/encoding/dot/encode_test.go b/graph/encoding/dot/encode_test.go index 7cd11c13..3a3a7330 100644 --- a/graph/encoding/dot/encode_test.go +++ b/graph/encoding/dot/encode_test.go @@ -9,6 +9,7 @@ import ( "gonum.org/v1/gonum/graph" "gonum.org/v1/gonum/graph/encoding" + "gonum.org/v1/gonum/graph/multi" "gonum.org/v1/gonum/graph/simple" ) @@ -1417,3 +1418,258 @@ func TestEncode(t *testing.T) { } } } + +type intlist []int64 + +func createMultigraph(g []intlist) graph.Multigraph { + dg := multi.NewUndirectedGraph() + for u, e := range g { + u := int64(u) + nu := multi.Node(u) + for _, v := range e { + nv := multi.Node(v) + dg.SetLine(dg.NewLine(nu, nv)) + } + } + return dg +} + +func createNamedMultigraph(g []intlist) graph.Multigraph { + dg := multi.NewUndirectedGraph() + for u, e := range g { + u := int64(u) + nu := namedNode{id: u, name: alpha[u : u+1]} + for _, v := range e { + nv := namedNode{id: v, name: alpha[v : v+1]} + dg.SetLine(dg.NewLine(nu, nv)) + } + } + return dg +} + +func createDirectedMultigraph(g []intlist) graph.Multigraph { + dg := multi.NewDirectedGraph() + for u, e := range g { + u := int64(u) + nu := multi.Node(u) + for _, v := range e { + nv := multi.Node(v) + dg.SetLine(dg.NewLine(nu, nv)) + } + } + return dg +} + +func createNamedDirectedMultigraph(g []intlist) graph.Multigraph { + dg := multi.NewDirectedGraph() + for u, e := range g { + u := int64(u) + nu := namedNode{id: u, name: alpha[u : u+1]} + for _, v := range e { + nv := namedNode{id: v, name: alpha[v : v+1]} + dg.SetLine(dg.NewLine(nu, nv)) + } + } + return dg +} + +var encodeMultiTests = []struct { + name string + g graph.Multigraph + + prefix string + + want string +}{ + { + g: createMultigraph([]intlist{}), + want: `graph { +}`, + }, + { + g: createMultigraph([]intlist{ + 0: {1}, + 1: {0, 2}, + 2: {}, + }), + want: `graph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -- 1; + 0 -- 1; + 1 -- 2; +}`, + }, + { + g: createMultigraph([]intlist{ + 0: {1}, + 1: {2, 2}, + 2: {0, 0, 0}, + }), + want: `graph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -- 1; + 0 -- 2; + 0 -- 2; + 0 -- 2; + 1 -- 2; + 1 -- 2; +}`, + }, + { + g: createNamedMultigraph([]intlist{ + 0: {1}, + 1: {2, 2}, + 2: {0, 0, 0}, + }), + want: `graph { + // Node definitions. + A; + B; + C; + + // Edge definitions. + A -- B; + A -- C; + A -- C; + A -- C; + B -- C; + B -- C; +}`, + }, + { + g: createMultigraph([]intlist{ + 0: {2, 1, 0}, + 1: {2, 1, 0}, + 2: {2, 1, 0}, + }), + want: `graph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -- 0; + 0 -- 1; + 0 -- 1; + 0 -- 2; + 0 -- 2; + 1 -- 1; + 1 -- 2; + 1 -- 2; + 2 -- 2; +}`, + }, + { + g: createDirectedMultigraph([]intlist{}), + want: `digraph { +}`, + }, + { + g: createDirectedMultigraph([]intlist{ + 0: {1}, + 1: {0, 2}, + 2: {}, + }), + want: `digraph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -> 1; + 1 -> 0; + 1 -> 2; +}`, + }, + { + g: createDirectedMultigraph([]intlist{ + 0: {1}, + 1: {2, 2}, + 2: {0, 0, 0}, + }), + want: `digraph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -> 1; + 1 -> 2; + 1 -> 2; + 2 -> 0; + 2 -> 0; + 2 -> 0; +}`, + }, + { + g: createNamedDirectedMultigraph([]intlist{ + 0: {1}, + 1: {2, 2}, + 2: {0, 0, 0}, + }), + want: `digraph { + // Node definitions. + A; + B; + C; + + // Edge definitions. + A -> B; + B -> C; + B -> C; + C -> A; + C -> A; + C -> A; +}`, + }, + { + g: createDirectedMultigraph([]intlist{ + 0: {2, 1, 0}, + 1: {2, 1, 0}, + 2: {2, 1, 0}, + }), + want: `digraph { + // Node definitions. + 0; + 1; + 2; + + // Edge definitions. + 0 -> 0; + 0 -> 1; + 0 -> 2; + 1 -> 0; + 1 -> 1; + 1 -> 2; + 2 -> 0; + 2 -> 1; + 2 -> 2; +}`, + }, +} + +func TestEncodeMulti(t *testing.T) { + for i, test := range encodeMultiTests { + got, err := MarshalMulti(test.g, test.name, test.prefix, "\t") + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if string(got) != test.want { + t.Errorf("unexpected DOT result for test %d:\ngot: %s\nwant:%s", i, got, test.want) + } + } +} diff --git a/graph/encoding/encoding.go b/graph/encoding/encoding.go index 33bda7eb..53ef0d56 100644 --- a/graph/encoding/encoding.go +++ b/graph/encoding/encoding.go @@ -12,6 +12,12 @@ type Builder interface { graph.Builder } +// MultiBuilder is a graph that can have user-defined nodes and edges added. +type MultiBuilder interface { + graph.Multigraph + graph.MultigraphBuilder +} + // AttributeSetter is implemented by types that can set an encoded graph // attribute. type AttributeSetter interface { diff --git a/graph/internal/ordered/sort.go b/graph/internal/ordered/sort.go index ea799931..a7250d1f 100644 --- a/graph/internal/ordered/sort.go +++ b/graph/internal/ordered/sort.go @@ -74,3 +74,20 @@ func Reverse(nodes []graph.Node) { nodes[i], nodes[j] = nodes[j], nodes[i] } } + +// LinesByIDs implements the sort.Interface sorting a slice of graph.LinesByIDs +// lexically by the From IDs, then by the To IDs, finally by the Line IDs. +type LinesByIDs []graph.Line + +func (n LinesByIDs) Len() int { return len(n) } +func (n LinesByIDs) Less(i, j int) bool { + a, b := n[i], n[j] + if a.From().ID() != b.From().ID() { + return a.From().ID() < b.From().ID() + } + if a.To().ID() != b.To().ID() { + return a.To().ID() < b.To().ID() + } + return n[i].ID() < n[j].ID() +} +func (n LinesByIDs) Swap(i, j int) { n[i], n[j] = n[j], n[i] }