Skip to content

Commit

Permalink
Add function to naturally sort device names
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew McDermott committed May 25, 2016
1 parent 9947bfc commit e5faf5c
Show file tree
Hide file tree
Showing 2 changed files with 308 additions and 0 deletions.
219 changes: 219 additions & 0 deletions network/devicenames.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package network

import (
"sort"
"strconv"
"unicode/utf8"
)

const (
EOF = iota
LITERAL
NUMBER
)

type token int

type deviceNameScanner struct {
src string

// scanning state
ch rune // current character
offset int // character offset
rdOffset int // reading offset (position of next ch)
}

type deviceName struct {
name string
tokens []int
}

type devices []deviceName

func (d devices) Len() int {
return len(d)
}

func (d devices) Less(i, j int) bool {
if r := intCompare(d[i].tokens, d[j].tokens); r == -1 {
return true
} else {
return false
}
}

func (d devices) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}

// adapted from runtime/noasm.go
func intCompare(s1, s2 []int) int {
l := len(s1)
if len(s2) < l {
l = len(s2)
}
if l == 0 || &s1[0] == &s2[0] {
goto samebytes
}
for i := 0; i < l; i++ {
c1, c2 := s1[i], s2[i]
if c1 < c2 {
return -1
}
if c1 > c2 {
return +1
}
}
samebytes:
if len(s1) < len(s2) {
return -1
}
if len(s1) > len(s2) {
return +1
}
return 0
}

func (s *deviceNameScanner) init(src string) {
s.src = src
s.ch = ' '
s.offset = 0
s.rdOffset = 0
s.next()
}

func (s *deviceNameScanner) next() {
if s.rdOffset < len(s.src) {
s.offset = s.rdOffset
r, w := rune(s.src[s.rdOffset]), 1
s.rdOffset += w
s.ch = r
} else {
s.offset = len(s.src)
s.ch = -1 // EOF
}
}

func (s *deviceNameScanner) peek() rune {
if s.rdOffset < len(s.src) {
r, _ := rune(s.src[s.rdOffset]), 1
return r
}
return -1
}

func isDigit(ch rune) bool {
return '0' <= ch && ch <= '9'
}

func (s *deviceNameScanner) scanNumber() string {
// Treat leading zeros as discrete numbers as this aids the
// natural sort ordering. We also only parse whole numbers;
// floating point values are considered an integer- and
// fractional-part.

if s.ch == '0' && s.peek() == '0' {
s.next()
return "0"
}

cur := s.offset

for isDigit(s.ch) {
s.next()
}

return string(s.src[cur:s.offset])
}

func (s *deviceNameScanner) scan() (tok token, lit string) {
switch ch := s.ch; {
case -1 == ch:
return EOF, ""
case '0' <= ch && ch <= '9':
return NUMBER, s.scanNumber()
default:
lit = string(s.ch)
s.next()
return LITERAL, lit
}
}

func parseDeviceName(src string) deviceName {
var s deviceNameScanner

s.init(src)

d := deviceName{name: src}

for {
tok, lit := s.scan()
switch tok {
case EOF:
return d
case LITERAL:
x, _ := utf8.DecodeRuneInString(lit)
d.tokens = append(d.tokens, int(x))
case NUMBER:
val, _ := strconv.Atoi(lit)
d.tokens = append(d.tokens, val)
}
}
}

func parseDeviceNames(args ...string) devices {
devices := make(devices, 0)

for _, a := range args {
devices = append(devices, parseDeviceName(a))
}

return devices
}

// NaturallySortDeviceNames returns an ordered list of names based on
// a natural ordering where 'natural' is an ordering of the string
// value in alphabetical order, execept that multi-digit numbers are
// ordered as a single character.
//
// For example, sorting:
//
// [ br-eth10 br-eth1 br-eth2 ]
//
// would sort as:
//
// [ br-eth1 br-eth2 br-eth10 ]
//
// In purely alphabetical sorting "br-eth10" would be sorted before
// "br-eth2" because "1" is sorted as smaller than "2", while in
// natural sorting "br-eth2" is sorted before "br-eth10" because "2"
// is sorted as smaller than "10".
//
// This also extends to multiply repeated numbers (e.g., VLANs).
//
// For example, sorting:
//
// [ br-eth2 br-eth10.10 br-eth200.0 br-eth1.0 br-eth2.0 ]
//
// would sort as:
//
// [ br-eth1.0 br-eth2 br-eth2.0 br-eth10.10 br-eth200.0 ]
//
func NaturallySortDeviceNames(names ...string) []string {
if names == nil {
return nil
}

devices := parseDeviceNames(names...)
sort.Sort(devices)
sortedNames := make([]string, len(devices))

for i, v := range devices {
sortedNames[i] = v.name
}

return sortedNames
}
89 changes: 89 additions & 0 deletions network/devicenames_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package network_test

import (
gc "gopkg.in/check.v1"

"github.com/juju/juju/network"
)

type DeviceNamesSuite struct{}

var _ = gc.Suite(&DeviceNamesSuite{})

func (s *DeviceNamesSuite) TestNaturallySortDeviceNames(c *gc.C) {
for i, test := range []struct {
message string
input []string
expected []string
}{{
message: "empty input, empty output",
input: []string{},
expected: []string{},
}, {
message: "nil input, nil output",
}, {
message: "one input",
input: []string{"a"},
expected: []string{"a"},
}, {
message: "two values, no numbers",
input: []string{"b", "a"},
expected: []string{"a", "b"},
}, {
message: "two values, mixed content",
input: []string{"b1", "a1"},
expected: []string{"a1", "b1"},
}, {
message: "identical values, numbers only",
input: []string{"1", "1", "1", "1"},
expected: []string{"1", "1", "1", "1"},
}, {
message: "identical values, mixed content",
input: []string{"a1", "a1", "a1", "a1"},
expected: []string{"a1", "a1", "a1", "a1"},
}, {
message: "reversed input",
input: []string{"a10", "a9", "a8", "a7", "a6", "a5", "a4", "a3", "a2", "a1", "a0"},
expected: []string{"a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"},
}, {
message: "multiple numbers per value",
input: []string{"a10.11", "a10.10", "a10.1"},
expected: []string{"a10.1", "a10.10", "a10.11"},
}, {
message: "value with leading zero",
input: []string{"a50", "a51.", "a50.31", "a50.4", "a5.034e1", "a50.300"},
expected: []string{"a5.034e1", "a50", "a50.4", "a50.31", "a50.300", "a51."},
}, {
message: "value with multiple leading zeros",
input: []string{"a50", "a51.", "a0050.31", "a50.4", "a5.034e1", "a00050.300"},
expected: []string{"a00050.300", "a0050.31", "a5.034e1", "a50", "a50.4", "a51."},
}, {
message: "strings with numbers in ascending order",
input: []string{"a2", "a5", "a9", "a1", "a4", "a10", "a6"},
expected: []string{"a1", "a2", "a4", "a5", "a6", "a9", "a10"},
}, {
message: "values that look like version numbers",
input: []string{"1.9.9a", "1.11", "1.9.9b", "1.11.4", "1.10.1"},
expected: []string{"1.9.9a", "1.9.9b", "1.10.1", "1.11", "1.11.4"},
}, {
message: "bridge device names",
input: []string{"br-eth10", "br-eth2", "br-eth1"},
expected: []string{"br-eth1", "br-eth2", "br-eth10"},
}, {
message: "bridge device names with VLAN numbers",
input: []string{"br-eth10.10", "br-eth2.10", "br-eth200", "br-eth1.100", "br-eth1.10"},
expected: []string{"br-eth1.10", "br-eth1.100", "br-eth2.10", "br-eth10.10", "br-eth200"},
}, {
message: "bridge device names with leading zero",
input: []string{"br-eth0", "br-eth10.10", "br-eth2.10", "br-eth1.100", "br-eth1.10", "br-eth10"},
expected: []string{"br-eth0", "br-eth1.10", "br-eth1.100", "br-eth2.10", "br-eth10", "br-eth10.10"},
}} {
c.Logf("%v: %s", i, test.message)
result := network.NaturallySortDeviceNames(test.input...)
c.Assert(result, gc.HasLen, len(test.input))
c.Assert(result, gc.DeepEquals, test.expected)
}
}

0 comments on commit e5faf5c

Please sign in to comment.