├── README.md ├── LICENSE.md ├── naturalsort.go └── naturalsort_test.go /README.md: -------------------------------------------------------------------------------- 1 | # naturalsort 2 | A simple natural string sorter for Go. 3 | 4 | ##Usage 5 | Implements the `sort.Interface` 6 | 7 | called by `sort.Sort(NaturalSort([]string))` 8 | ###Example 9 | 10 | ```go 11 | SampleStringArray := []string{ 12 | "z24", "z2", "z15", "z1", 13 | "z3", "z20", "z5", "z11", 14 | "z 21", "z22"} 15 | sort.Sort(NaturalSort(SampleStringArray)) 16 | ``` 17 | 18 | ##Needless Description 19 | Inspired by [Jeff Atwood's seminal blog post](http://blog.codinghorror.com/sorting-for-humans-natural-sort-order/) and 20 | structured similarly to [Ian Griffiths' C# implementation](http://www.interact-sw.co.uk/iangblog/2007/12/13/natural-sorting). 21 | This uses a regex to split the numeric and non-numeric portions of the string into a chunky array. Next, the left and right sides' 22 | chunks are compared either by string comparrison (if either chunk is a non-numeric), or by ~~integer (if both chunks are numeric)~~ a character-by-character iterative function that compares numerical strings 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 skarademir 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /naturalsort.go: -------------------------------------------------------------------------------- 1 | package naturalsort 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type NaturalSort []string 9 | 10 | var r = regexp.MustCompile(`[^0-9]+|[0-9]+`) 11 | 12 | func (s NaturalSort) Len() int { 13 | return len(s) 14 | } 15 | func (s NaturalSort) Swap(i, j int) { 16 | s[i], s[j] = s[j], s[i] 17 | } 18 | func (s NaturalSort) Less(i, j int) bool { 19 | 20 | spliti := r.FindAllString(strings.Replace(s[i], " ", "", -1), -1) 21 | splitj := r.FindAllString(strings.Replace(s[j], " ", "", -1), -1) 22 | 23 | for index := 0; index < len(spliti) && index < len(splitj); index++ { 24 | if spliti[index] != splitj[index] { 25 | // Both slices are numbers 26 | if isNumber(spliti[index][0]) && isNumber(splitj[index][0]) { 27 | // Remove Leading Zeroes 28 | stringi := strings.TrimLeft(spliti[index], "0") 29 | stringj := strings.TrimLeft(splitj[index], "0") 30 | if len(stringi) == len(stringj) { 31 | for indexchar := 0; indexchar < len(stringi); indexchar++ { 32 | if stringi[indexchar] != stringj[indexchar] { 33 | return stringi[indexchar] < stringj[indexchar] 34 | } 35 | } 36 | return len(spliti[index]) < len(splitj[index]) 37 | } 38 | return len(stringi) < len(stringj) 39 | } 40 | // One of the slices is a number (we give precedence to numbers regardless of ASCII table position) 41 | if isNumber(spliti[index][0]) || isNumber(splitj[index][0]) { 42 | return isNumber(spliti[index][0]) 43 | } 44 | // Both slices are not numbers 45 | return spliti[index] < splitj[index] 46 | } 47 | 48 | } 49 | // Fall back for cases where space characters have been annihliated by the replacment call 50 | // Here we iterate over the unmolsested string and prioritize numbers over 51 | for index := 0; index < len(s[i]) && index < len(s[j]); index++ { 52 | if isNumber(s[i][index]) || isNumber(s[j][index]) { 53 | return isNumber(s[i][index]) 54 | } 55 | } 56 | return s[i] < s[j] 57 | } 58 | func isNumber(input uint8) bool { 59 | return input >= '0' && input <= '9' 60 | } 61 | -------------------------------------------------------------------------------- /naturalsort_test.go: -------------------------------------------------------------------------------- 1 | package naturalsort 2 | 3 | import ( 4 | "reflect" 5 | "sort" 6 | "testing" 7 | ) 8 | 9 | func TestSortValid(t *testing.T) { 10 | cases := []struct { 11 | data, expected []string 12 | }{ 13 | { 14 | nil, 15 | nil, 16 | }, 17 | { 18 | []string{}, 19 | []string{}, 20 | }, 21 | { 22 | []string{"a"}, 23 | []string{"a"}, 24 | }, 25 | { 26 | []string{"0"}, 27 | []string{"0"}, 28 | }, 29 | { 30 | []string{"data", "data20", "data3"}, 31 | []string{"data", "data3", "data20"}, 32 | }, 33 | { 34 | []string{"1", "2", "30", "22", "0", "00", "3"}, 35 | []string{"0", "00", "1", "2", "3", "22", "30"}, 36 | }, 37 | { 38 | []string{"A1", "A0", "A21", "A11", "A111", "A2"}, 39 | []string{"A0", "A1", "A2", "A11", "A21", "A111"}, 40 | }, 41 | { 42 | []string{"A1BA1", "A11AA1", "A2AB0", "B1AA1", "A1AA1"}, 43 | []string{"A1AA1", "A1BA1", "A2AB0", "A11AA1", "B1AA1"}, 44 | }, 45 | { 46 | []string{"1ax10", "1a10", "1ax2", "1ax"}, 47 | []string{"1a10", "1ax", "1ax2", "1ax10"}, 48 | }, 49 | { 50 | []string{"z1a10", "z1ax2", "z1ax"}, 51 | []string{"z1a10", "z1ax", "z1ax2"}, 52 | }, 53 | { 54 | // regression test for #8 55 | []string{"a0000001", "a0001"}, 56 | []string{"a0001", "a0000001"}, 57 | }, 58 | { 59 | // regression test for #10 - Number sort before any symbols even if theyre lower on the ASCII table 60 | []string{"#1", "1", "_1", "a"}, 61 | []string{"1", "#1", "_1", "a"}, 62 | }, 63 | { 64 | // regression test for #10 - Number sort before any symbols even if theyre lower on the ASCII table 65 | []string{"#1", "1", "_1", "a"}, 66 | []string{"1", "#1", "_1", "a"}, 67 | }, 68 | { // test correct handling of space-only strings 69 | []string{"1", " ", "0"}, 70 | []string{"0", "1", " "}, 71 | }, 72 | { // test correct handling of multiple spaces being correctly ordered AFTER numbers 73 | []string{"1", " ", " 1", " "}, 74 | []string{"1", " ", " 1", " "}, 75 | }, 76 | { 77 | []string{"1", "#1", "a#", "a1"}, 78 | []string{"1", "#1", "a1", "a#"}, 79 | }, 80 | { 81 | // regression test for #10 82 | []string{"111111111111111111112", "111111111111111111113", "1111111111111111111120"}, 83 | []string{"111111111111111111112", "111111111111111111113", "1111111111111111111120"}, 84 | }, 85 | } 86 | 87 | for i, c := range cases { 88 | sort.Sort(NaturalSort(c.data)) 89 | if !reflect.DeepEqual(c.data, c.expected) { 90 | t.Fatalf("Wrong order in test case #%d.\nExpected=%v\nGot=%v", i, c.expected, c.data) 91 | } 92 | } 93 | 94 | } 95 | 96 | func BenchmarkSort(b *testing.B) { 97 | var data = [...]string{"A1BA1", "A11AA1", "A2AB0", "B1AA1", "A1AA1"} 98 | for ii := 0; ii < b.N; ii++ { 99 | d := NaturalSort(data[:]) 100 | sort.Sort(d) 101 | } 102 | } 103 | --------------------------------------------------------------------------------