mirror of
https://github.com/gonum/gonum.git
synced 2025-10-06 15:47:01 +08:00
graph/encoding: provide attribute handling out of the box
This commit is contained in:
@@ -532,7 +532,13 @@ type dotDirectedGraph struct {
|
||||
// newDotDirectedGraph returns a new directed capable of creating user-defined
|
||||
// nodes and edges.
|
||||
func newDotDirectedGraph() *dotDirectedGraph {
|
||||
return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph()}
|
||||
return &dotDirectedGraph{
|
||||
DirectedGraph: simple.NewDirectedGraph(),
|
||||
|
||||
graph: &encoding.Attributes{},
|
||||
node: &encoding.Attributes{},
|
||||
edge: &encoding.Attributes{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewNode returns a new node with a unique node ID for the graph.
|
||||
@@ -552,7 +558,7 @@ func (g *dotDirectedGraph) DOTAttributers() (graph, node, edge encoding.Attribut
|
||||
|
||||
// DOTAttributeSetters implements the dot.AttributeSetters interface.
|
||||
func (g *dotDirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
||||
return &g.graph, &g.node, &g.edge
|
||||
return g.graph, g.node, g.edge
|
||||
}
|
||||
|
||||
// SetDOTID sets the DOT ID of the graph.
|
||||
@@ -579,7 +585,13 @@ type dotUndirectedGraph struct {
|
||||
// newDotUndirectedGraph returns a new undirected capable of creating user-
|
||||
// defined nodes and edges.
|
||||
func newDotUndirectedGraph() *dotUndirectedGraph {
|
||||
return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph()}
|
||||
return &dotUndirectedGraph{
|
||||
UndirectedGraph: simple.NewUndirectedGraph(),
|
||||
|
||||
graph: &encoding.Attributes{},
|
||||
node: &encoding.Attributes{},
|
||||
edge: &encoding.Attributes{},
|
||||
}
|
||||
}
|
||||
|
||||
// NewNode adds a new node with a unique node ID to the graph.
|
||||
@@ -599,7 +611,7 @@ func (g *dotUndirectedGraph) DOTAttributers() (graph, node, edge encoding.Attrib
|
||||
|
||||
// DOTUnmarshalerAttrs implements the dot.UnmarshalerAttrs interface.
|
||||
func (g *dotUndirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
||||
return &g.graph, &g.node, &g.edge
|
||||
return g.graph, g.node, g.edge
|
||||
}
|
||||
|
||||
// SetDOTID sets the DOT ID of the graph.
|
||||
@@ -705,15 +717,9 @@ func (e *dotEdge) ToPort() (port, compass string) {
|
||||
return e.ToPortLabels.Port, e.ToPortLabels.Compass
|
||||
}
|
||||
|
||||
// attributes is a helper for global attributes.
|
||||
type attributes []encoding.Attribute
|
||||
|
||||
func (a attributes) Attributes() []encoding.Attribute {
|
||||
return []encoding.Attribute(a)
|
||||
}
|
||||
func (a *attributes) SetAttribute(attr encoding.Attribute) error {
|
||||
*a = append(*a, attr)
|
||||
return nil
|
||||
type attributes interface {
|
||||
encoding.Attributer
|
||||
encoding.AttributeSetter
|
||||
}
|
||||
|
||||
const undirectedSelfLoopGraph = `graph {
|
||||
|
@@ -321,10 +321,6 @@ type graphAttributer struct {
|
||||
edge encoding.Attributer
|
||||
}
|
||||
|
||||
type attributer []encoding.Attribute
|
||||
|
||||
func (a attributer) Attributes() []encoding.Attribute { return a }
|
||||
|
||||
func (g graphAttributer) DOTAttributers() (graph, node, edge encoding.Attributer) {
|
||||
return g.graph, g.node, g.edge
|
||||
}
|
||||
@@ -1192,8 +1188,8 @@ var encodeTests = []struct {
|
||||
{from: 2, to: 4}: {},
|
||||
{from: 3, to: 4}: {{Key: "color", Value: "red"}},
|
||||
}),
|
||||
graph: attributer{{Key: "rankdir", Value: `"LR"`}},
|
||||
node: attributer{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}},
|
||||
graph: &encoding.Attributes{{Key: "rankdir", Value: `"LR"`}},
|
||||
node: &encoding.Attributes{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}},
|
||||
edge: nil,
|
||||
},
|
||||
|
||||
|
@@ -34,3 +34,36 @@ type Attributer interface {
|
||||
type Attribute struct {
|
||||
Key, Value string
|
||||
}
|
||||
|
||||
// Attributes is a helper type providing simple attribute handling.
|
||||
type Attributes []Attribute
|
||||
|
||||
// Attributes returns all of the receiver's attributes.
|
||||
func (a *Attributes) Attributes() []Attribute {
|
||||
return *a
|
||||
}
|
||||
|
||||
// SetAttribute sets attr in the receiver. Calling SetAttribute with an
|
||||
// Attribute with a Key that is in the collection replaces the existing
|
||||
// value and calling with an empty Value removes the attribute from the
|
||||
// collection if it exists. SetAttribute always returns nil.
|
||||
func (a *Attributes) SetAttribute(attr Attribute) error {
|
||||
if attr.Key == "" {
|
||||
return nil
|
||||
}
|
||||
for i, v := range *a {
|
||||
if v.Key == attr.Key {
|
||||
if attr.Value == "" {
|
||||
(*a)[i] = (*a)[len(*a)-1]
|
||||
*a = (*a)[:len(*a)-1]
|
||||
return nil
|
||||
}
|
||||
(*a)[i].Value = attr.Value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if attr.Value != "" {
|
||||
*a = append(*a, attr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
121
graph/encoding/encoding_test.go
Normal file
121
graph/encoding/encoding_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright ©2021 The Gonum Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package encoding
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var setAttributesTests = []struct {
|
||||
attr *Attributes
|
||||
opName string
|
||||
op func(AttributeSetter) error
|
||||
want *Attributes
|
||||
}{
|
||||
{
|
||||
attr: &Attributes{},
|
||||
opName: "noop",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "", Value: "bar"})
|
||||
},
|
||||
want: &Attributes{},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{},
|
||||
opName: "add attr to empty",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "foo", Value: "bar"})
|
||||
},
|
||||
want: &Attributes{{Key: "foo", Value: "bar"}},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{},
|
||||
opName: "remove attr from empty",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "foo", Value: ""})
|
||||
},
|
||||
want: &Attributes{},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}},
|
||||
opName: "add attr to non-empty",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "bif", Value: "fud"})
|
||||
},
|
||||
want: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}},
|
||||
opName: "remove attr from singleton",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "foo", Value: ""})
|
||||
},
|
||||
want: &Attributes{},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}},
|
||||
opName: "remove first attr from pair",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "foo", Value: ""})
|
||||
},
|
||||
want: &Attributes{{Key: "bif", Value: "fud"}},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}},
|
||||
opName: "remove second attr from pair",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "bif", Value: ""})
|
||||
},
|
||||
want: &Attributes{{Key: "foo", Value: "bar"}},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}},
|
||||
opName: "replace first attr in pair",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "foo", Value: "not bar"})
|
||||
},
|
||||
want: &Attributes{{Key: "foo", Value: "not bar"}, {Key: "bif", Value: "fud"}},
|
||||
},
|
||||
{
|
||||
attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}},
|
||||
opName: "replace second attr in pair",
|
||||
op: func(a AttributeSetter) error {
|
||||
return a.SetAttribute(Attribute{Key: "bif", Value: "not fud"})
|
||||
},
|
||||
want: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "not fud"}},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSetAttributes(t *testing.T) {
|
||||
for _, test := range setAttributesTests {
|
||||
err := test.op(test.attr)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error for %q: %v", test.opName, err)
|
||||
continue
|
||||
}
|
||||
if !sameAttributes(test.attr, test.want) {
|
||||
t.Errorf("unexpected result from %q:\ngot: %+v\nwant:%+v", test.opName, test.attr, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sameAttributes(a, b Attributer) bool {
|
||||
aAttr := a.Attributes()
|
||||
bAttr := b.Attributes()
|
||||
if len(aAttr) != len(bAttr) {
|
||||
return false
|
||||
}
|
||||
aAttr = append(aAttr[:0:0], aAttr...)
|
||||
sort.Slice(aAttr, func(i, j int) bool { return aAttr[i].Key < aAttr[j].Key })
|
||||
bAttr = append(bAttr[:0:0], bAttr...)
|
||||
sort.Slice(bAttr, func(i, j int) bool { return bAttr[i].Key < bAttr[j].Key })
|
||||
for i, a := range aAttr {
|
||||
if bAttr[i] != a {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
Reference in New Issue
Block a user