├── LICENSE ├── natsort.go ├── README.md └── natsort_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Vincent Batoufflet and Marc Falzon 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * Neither the name of the authors nor the names of its contributors 16 | may be used to endorse or promote products derived from this software 17 | without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 22 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 23 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 24 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 25 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 26 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 27 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 28 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /natsort.go: -------------------------------------------------------------------------------- 1 | // Package natsort implements natural strings sorting 2 | package natsort 3 | 4 | import ( 5 | "regexp" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | type stringSlice []string 11 | 12 | func (s stringSlice) Len() int { 13 | return len(s) 14 | } 15 | 16 | func (s stringSlice) Less(a, b int) bool { 17 | return Compare(s[a], s[b]) 18 | } 19 | 20 | func (s stringSlice) Swap(a, b int) { 21 | s[a], s[b] = s[b], s[a] 22 | } 23 | 24 | var chunkifyRegexp = regexp.MustCompile(`(\d+|\D+)`) 25 | 26 | func chunkify(s string) []string { 27 | return chunkifyRegexp.FindAllString(s, -1) 28 | } 29 | 30 | // Sort sorts a list of strings in a natural order 31 | func Sort(l []string) { 32 | sort.Sort(stringSlice(l)) 33 | } 34 | 35 | // Compare returns true if the first string precedes the second one according to natural order 36 | func Compare(a, b string) bool { 37 | chunksA := chunkify(a) 38 | chunksB := chunkify(b) 39 | 40 | nChunksA := len(chunksA) 41 | nChunksB := len(chunksB) 42 | 43 | for i := range chunksA { 44 | if i >= nChunksB { 45 | return false 46 | } 47 | 48 | aInt, aErr := strconv.Atoi(chunksA[i]) 49 | bInt, bErr := strconv.Atoi(chunksB[i]) 50 | 51 | // If both chunks are numeric, compare them as integers 52 | if aErr == nil && bErr == nil { 53 | if aInt == bInt { 54 | if i == nChunksA-1 { 55 | // We reached the last chunk of A, thus B is greater than A 56 | return true 57 | } else if i == nChunksB-1 { 58 | // We reached the last chunk of B, thus A is greater than B 59 | return false 60 | } 61 | 62 | continue 63 | } 64 | 65 | return aInt < bInt 66 | } 67 | 68 | // So far both strings are equal, continue to next chunk 69 | if chunksA[i] == chunksB[i] { 70 | if i == nChunksA-1 { 71 | // We reached the last chunk of A, thus B is greater than A 72 | return true 73 | } else if i == nChunksB-1 { 74 | // We reached the last chunk of B, thus A is greater than B 75 | return false 76 | } 77 | 78 | continue 79 | } 80 | 81 | return chunksA[i] < chunksB[i] 82 | } 83 | 84 | return false 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # natsort: natural strings sorting in Go 2 | 3 | This is an implementation of the "Alphanum Algorithm" by [Dave Koelle][0] in Go. 4 | 5 | [![GoDoc](https://godoc.org/facette.io/natsort?status.svg)](https://godoc.org/facette.io/natsort) 6 | 7 | ## Usage 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "strings" 15 | 16 | "facette.io/natsort" 17 | ) 18 | 19 | func main() { 20 | list := []string{ 21 | "1000X Radonius Maximus", 22 | "10X Radonius", 23 | "200X Radonius", 24 | "20X Radonius", 25 | "20X Radonius Prime", 26 | "30X Radonius", 27 | "40X Radonius", 28 | "Allegia 50 Clasteron", 29 | "Allegia 500 Clasteron", 30 | "Allegia 50B Clasteron", 31 | "Allegia 51 Clasteron", 32 | "Allegia 6R Clasteron", 33 | "Alpha 100", 34 | "Alpha 2", 35 | "Alpha 200", 36 | "Alpha 2A", 37 | "Alpha 2A-8000", 38 | "Alpha 2A-900", 39 | "Callisto Morphamax", 40 | "Callisto Morphamax 500", 41 | "Callisto Morphamax 5000", 42 | "Callisto Morphamax 600", 43 | "Callisto Morphamax 6000 SE", 44 | "Callisto Morphamax 6000 SE2", 45 | "Callisto Morphamax 700", 46 | "Callisto Morphamax 7000", 47 | "Xiph Xlater 10000", 48 | "Xiph Xlater 2000", 49 | "Xiph Xlater 300", 50 | "Xiph Xlater 40", 51 | "Xiph Xlater 5", 52 | "Xiph Xlater 50", 53 | "Xiph Xlater 500", 54 | "Xiph Xlater 5000", 55 | "Xiph Xlater 58", 56 | } 57 | 58 | natsort.Sort(list) 59 | 60 | fmt.Println(strings.Join(list, "\n")) 61 | } 62 | ``` 63 | 64 | Output: 65 | 66 | ``` 67 | 10X Radonius 68 | 20X Radonius 69 | 20X Radonius Prime 70 | 30X Radonius 71 | 40X Radonius 72 | 200X Radonius 73 | 1000X Radonius Maximus 74 | Allegia 6R Clasteron 75 | Allegia 50 Clasteron 76 | Allegia 50B Clasteron 77 | Allegia 51 Clasteron 78 | Allegia 500 Clasteron 79 | Alpha 2 80 | Alpha 2A 81 | Alpha 2A-900 82 | Alpha 2A-8000 83 | Alpha 100 84 | Alpha 200 85 | Callisto Morphamax 86 | Callisto Morphamax 500 87 | Callisto Morphamax 600 88 | Callisto Morphamax 700 89 | Callisto Morphamax 5000 90 | Callisto Morphamax 6000 SE 91 | Callisto Morphamax 6000 SE2 92 | Callisto Morphamax 7000 93 | Xiph Xlater 5 94 | Xiph Xlater 40 95 | Xiph Xlater 50 96 | Xiph Xlater 58 97 | Xiph Xlater 300 98 | Xiph Xlater 500 99 | Xiph Xlater 2000 100 | Xiph Xlater 5000 101 | Xiph Xlater 10000 102 | ``` 103 | 104 | [0]: http://davekoelle.com/alphanum.html 105 | -------------------------------------------------------------------------------- /natsort_test.go: -------------------------------------------------------------------------------- 1 | package natsort 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | var testList = []string{ 10 | "1000X Radonius Maximus", 11 | "10X Radonius", 12 | "200X Radonius", 13 | "20X Radonius", 14 | "20X Radonius Prime", 15 | "30X Radonius", 16 | "40X Radonius", 17 | "Allegia 50 Clasteron", 18 | "Allegia 500 Clasteron", 19 | "Allegia 50B Clasteron", 20 | "Allegia 51 Clasteron", 21 | "Allegia 6R Clasteron", 22 | "Alpha 100", 23 | "Alpha 2", 24 | "Alpha 200", 25 | "Alpha 2A", 26 | "Alpha 2A-8000", 27 | "Alpha 2A-900", 28 | "Callisto Morphamax", 29 | "Callisto Morphamax 500", 30 | "Callisto Morphamax 5000", 31 | "Callisto Morphamax 600", 32 | "Callisto Morphamax 6000 SE", 33 | "Callisto Morphamax 6000 SE2", 34 | "Callisto Morphamax 700", 35 | "Callisto Morphamax 7000", 36 | "Xiph Xlater 10000", 37 | "Xiph Xlater 2000", 38 | "Xiph Xlater 300", 39 | "Xiph Xlater 40", 40 | "Xiph Xlater 5", 41 | "Xiph Xlater 50", 42 | "Xiph Xlater 500", 43 | "Xiph Xlater 5000", 44 | "Xiph Xlater 58", 45 | } 46 | 47 | func Test_Sort1(t *testing.T) { 48 | testListSortedOK := []string{ 49 | "10X Radonius", 50 | "20X Radonius", 51 | "20X Radonius Prime", 52 | "30X Radonius", 53 | "40X Radonius", 54 | "200X Radonius", 55 | "1000X Radonius Maximus", 56 | "Allegia 6R Clasteron", 57 | "Allegia 50 Clasteron", 58 | "Allegia 50B Clasteron", 59 | "Allegia 51 Clasteron", 60 | "Allegia 500 Clasteron", 61 | "Alpha 2", 62 | "Alpha 2A", 63 | "Alpha 2A-900", 64 | "Alpha 2A-8000", 65 | "Alpha 100", 66 | "Alpha 200", 67 | "Callisto Morphamax", 68 | "Callisto Morphamax 500", 69 | "Callisto Morphamax 600", 70 | "Callisto Morphamax 700", 71 | "Callisto Morphamax 5000", 72 | "Callisto Morphamax 6000 SE", 73 | "Callisto Morphamax 6000 SE2", 74 | "Callisto Morphamax 7000", 75 | "Xiph Xlater 5", 76 | "Xiph Xlater 40", 77 | "Xiph Xlater 50", 78 | "Xiph Xlater 58", 79 | "Xiph Xlater 300", 80 | "Xiph Xlater 500", 81 | "Xiph Xlater 2000", 82 | "Xiph Xlater 5000", 83 | "Xiph Xlater 10000", 84 | } 85 | testListSorted := testList[:] 86 | Sort(testListSorted) 87 | 88 | if !reflect.DeepEqual(testListSortedOK, testListSorted) { 89 | t.Fatalf(`ERROR: sorted list different from expected results: 90 | Expected results: 91 | %v 92 | 93 | Got: 94 | %v`, strings.Join(testListSortedOK, "\n"), strings.Join(testListSorted, "\n")) 95 | } 96 | } 97 | 98 | func Test_Sort2(t *testing.T) { 99 | testList := []string{ 100 | "z1.doc", 101 | "z10.doc", 102 | "z100.doc", 103 | "z101.doc", 104 | "z102.doc", 105 | "z11.doc", 106 | "z12.doc", 107 | "z13.doc", 108 | "z14.doc", 109 | "z15.doc", 110 | "z16.doc", 111 | "z17.doc", 112 | "z18.doc", 113 | "z19.doc", 114 | "z2.doc", 115 | "z20.doc", 116 | "z3.doc", 117 | "z4.doc", 118 | "z5.doc", 119 | "z6.doc", 120 | "z7.doc", 121 | "z8.doc", 122 | "z9.doc", 123 | } 124 | 125 | testListSortedOK := []string{ 126 | "z1.doc", 127 | "z2.doc", 128 | "z3.doc", 129 | "z4.doc", 130 | "z5.doc", 131 | "z6.doc", 132 | "z7.doc", 133 | "z8.doc", 134 | "z9.doc", 135 | "z10.doc", 136 | "z11.doc", 137 | "z12.doc", 138 | "z13.doc", 139 | "z14.doc", 140 | "z15.doc", 141 | "z16.doc", 142 | "z17.doc", 143 | "z18.doc", 144 | "z19.doc", 145 | "z20.doc", 146 | "z100.doc", 147 | "z101.doc", 148 | "z102.doc", 149 | } 150 | 151 | testListSorted := testList[:] 152 | Sort(testListSorted) 153 | 154 | if !reflect.DeepEqual(testListSortedOK, testListSorted) { 155 | t.Fatalf(`ERROR: sorted list different from expected results: 156 | Expected results: 157 | %v 158 | 159 | Got: 160 | %v`, strings.Join(testListSortedOK, "\n"), strings.Join(testListSorted, "\n")) 161 | } 162 | } 163 | 164 | func BenchmarkSort1(b *testing.B) { 165 | for n := 0; n < b.N; n++ { 166 | Sort(testList) 167 | } 168 | } 169 | --------------------------------------------------------------------------------