-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add function to naturally sort device names
- Loading branch information
Andrew McDermott
committed
May 25, 2016
1 parent
9947bfc
commit e5faf5c
Showing
2 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |