mirror of
https://github.com/gonum/gonum.git
synced 2025-10-06 23:52:47 +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
|
// newDotDirectedGraph returns a new directed capable of creating user-defined
|
||||||
// nodes and edges.
|
// nodes and edges.
|
||||||
func newDotDirectedGraph() *dotDirectedGraph {
|
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.
|
// 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.
|
// DOTAttributeSetters implements the dot.AttributeSetters interface.
|
||||||
func (g *dotDirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
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.
|
// SetDOTID sets the DOT ID of the graph.
|
||||||
@@ -579,7 +585,13 @@ type dotUndirectedGraph struct {
|
|||||||
// newDotUndirectedGraph returns a new undirected capable of creating user-
|
// newDotUndirectedGraph returns a new undirected capable of creating user-
|
||||||
// defined nodes and edges.
|
// defined nodes and edges.
|
||||||
func newDotUndirectedGraph() *dotUndirectedGraph {
|
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.
|
// 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.
|
// DOTUnmarshalerAttrs implements the dot.UnmarshalerAttrs interface.
|
||||||
func (g *dotUndirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) {
|
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.
|
// 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
|
return e.ToPortLabels.Port, e.ToPortLabels.Compass
|
||||||
}
|
}
|
||||||
|
|
||||||
// attributes is a helper for global attributes.
|
type attributes interface {
|
||||||
type attributes []encoding.Attribute
|
encoding.Attributer
|
||||||
|
encoding.AttributeSetter
|
||||||
func (a attributes) Attributes() []encoding.Attribute {
|
|
||||||
return []encoding.Attribute(a)
|
|
||||||
}
|
|
||||||
func (a *attributes) SetAttribute(attr encoding.Attribute) error {
|
|
||||||
*a = append(*a, attr)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const undirectedSelfLoopGraph = `graph {
|
const undirectedSelfLoopGraph = `graph {
|
||||||
|
@@ -321,10 +321,6 @@ type graphAttributer struct {
|
|||||||
edge encoding.Attributer
|
edge encoding.Attributer
|
||||||
}
|
}
|
||||||
|
|
||||||
type attributer []encoding.Attribute
|
|
||||||
|
|
||||||
func (a attributer) Attributes() []encoding.Attribute { return a }
|
|
||||||
|
|
||||||
func (g graphAttributer) DOTAttributers() (graph, node, edge encoding.Attributer) {
|
func (g graphAttributer) DOTAttributers() (graph, node, edge encoding.Attributer) {
|
||||||
return g.graph, g.node, g.edge
|
return g.graph, g.node, g.edge
|
||||||
}
|
}
|
||||||
@@ -1192,8 +1188,8 @@ var encodeTests = []struct {
|
|||||||
{from: 2, to: 4}: {},
|
{from: 2, to: 4}: {},
|
||||||
{from: 3, to: 4}: {{Key: "color", Value: "red"}},
|
{from: 3, to: 4}: {{Key: "color", Value: "red"}},
|
||||||
}),
|
}),
|
||||||
graph: attributer{{Key: "rankdir", Value: `"LR"`}},
|
graph: &encoding.Attributes{{Key: "rankdir", Value: `"LR"`}},
|
||||||
node: attributer{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}},
|
node: &encoding.Attributes{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}},
|
||||||
edge: nil,
|
edge: nil,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -34,3 +34,36 @@ type Attributer interface {
|
|||||||
type Attribute struct {
|
type Attribute struct {
|
||||||
Key, Value string
|
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