├── .gitignore ├── LICENSE ├── README.md ├── bench_test.go ├── constraints.go ├── floats ├── README.md ├── float_bench_test.go ├── float_sorter.go ├── float_sorter_test.go ├── floats.go ├── floats_test.go └── floats_unsafe.go ├── go.mod ├── go.sum ├── internal ├── internal.go └── internal_test.go ├── sorter.go ├── sorter_test.go ├── zermelo.go └── zermelo_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | count.out 22 | *.exe 23 | *.test 24 | .idea/ 25 | push.sh 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Shawn Smith 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zermelo v2 2 | ========= 3 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/shawnsmithdev/zermelo/v2) 4 | [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/shawnsmithdev/zermelo/master/LICENSE) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/shawnsmithdev/zermelo/v2)](https://goreportcard.com/report/github.com/shawnsmithdev/zermelo/v2) 6 | 7 | A radix sorting library for Go. Trade memory for speed! Now with more generics! 8 | 9 | ```go 10 | import "github.com/shawnsmithdev/zermelo/v2" 11 | 12 | func foo(large []uint64) 13 | zermelo.Sort(large) 14 | } 15 | ``` 16 | 17 | About 18 | ===== 19 | 20 | Zermelo is a sorting library featuring implementations of 21 | [radix sort](https://en.wikipedia.org/wiki/Radix_sort "Radix Sort"). I am especially influenced here by 22 | [these](http://codercorner.com/RadixSortRevisited.htm "Radix Sort Revisited") 23 | [two](http://stereopsis.com/radix.html "Radix Tricks") articles that describe various optimizations and how to work 24 | around the typical limitations of radix sort. 25 | 26 | You will generally only want to use zermelo if you won't mind the extra memory used for buffers and your application 27 | frequently sorts slices of supported types with at least 256 elements (128 for 32-bit types, 386 for `[]float64`). 28 | The larger the slices you are sorting, the more benefit you will gain by using zermelo instead of the standard library's 29 | in-place comparison sort [`slices.Sort()`](https://pkg.go.dev/slices#Sort). 30 | 31 | Etymology 32 | --------- 33 | Zermelo is named after [Ernst Zermelo](http://en.wikipedia.org/wiki/Ernst_Zermelo), who developed the proof for the 34 | [well-ordering theorem](https://en.wikipedia.org/wiki/Well-ordering_theorem). 35 | 36 | Supported Types 37 | =============== 38 | `Sort` and `NewSorter` support integer slices, that is `[]int`, `[]uint64`, `[]byte`, etc, and derived types. 39 | 40 | Sorter 41 | ====== 42 | 43 | A `Sorter` returned by `NewSorter` will reuse buffers created during `Sort()` calls. This is not thread safe. 44 | Buffers are grown as needed at a 25% exponential growth rate. This means if you sort a slice of size `n`, 45 | subsequent calls with slices up to `n * 1.25` in length will not cause another buffer allocation. This does not apply 46 | to the first allocation, which will make a buffer of the same size as the requested slice. This way, if the slices being 47 | sorted do not grow in size, there is no unused buffer space. 48 | 49 | ```go 50 | import "github.com/shawnsmithdev/zermelo/v2" 51 | 52 | func foo(bar [][]uint64) { 53 | sorter := zermelo.NewSorter[uint64]() 54 | for _, x := range bar { 55 | sorter.Sort(x) 56 | } 57 | } 58 | 59 | ``` 60 | 61 | Float Subpackage 62 | ================ 63 | `SortFloats` and `FloatSorter` provided in the `floats` subpackage support float slices, 64 | specifically `[]float32` and `[]float64` and derived types. 65 | This uses the unsafe package to treat floats as though they were unsigned integers. 66 | 67 | ```go 68 | import "github.com/shawnsmithdev/zermelo/v2/floats" 69 | 70 | func foo(bar [][]floats64) { 71 | sorter := floats.NewFloatSorter[float64]() 72 | for _, x := range bar { 73 | sorter.Sort(x) 74 | } 75 | } 76 | ``` 77 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package zermelo 2 | 3 | import ( 4 | "cmp" 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "runtime" 7 | "slices" 8 | "sort" 9 | "sync" 10 | "testing" 11 | ) 12 | 13 | // Benchmarks 14 | const testTinySize = compSortCutoff 15 | const testSmallSize = compSortCutoff64 16 | const testMediumSize = 1024 // ~1k * 64bit = 8 KB 17 | const testLargeSize = 1 << 20 // ~1M * 64bit = 8 MB 18 | 19 | // tiny32 20 | func BenchmarkSortSortInt32T(b *testing.B) { 21 | testSortBencher[int32](b, testTinySize, sortSort[int32]) 22 | } 23 | func BenchmarkSlicesSortInt32T(b *testing.B) { 24 | testSortBencher[int32](b, testTinySize, slices.Sort[[]int32, int32]) 25 | } 26 | func BenchmarkZSortInt32T(b *testing.B) { 27 | testSortBencher[int32](b, testTinySize, Sort[int32]) 28 | } 29 | func BenchmarkZSorterInt32T(b *testing.B) { 30 | testSortBencher[int32](b, testTinySize, newSorter[int32]().withCutoff(0).Sort) 31 | } 32 | 33 | // tiny 34 | func BenchmarkSortSortUint64T(b *testing.B) { 35 | testSortBencher[uint64](b, testTinySize, sortSort[uint64]) 36 | } 37 | func BenchmarkSlicesSortUint64T(b *testing.B) { 38 | testSortBencher[uint64](b, testTinySize, slices.Sort[[]uint64, uint64]) 39 | } 40 | func BenchmarkZSortUint64T(b *testing.B) { 41 | testSortBencher[uint64](b, testTinySize, Sort[uint64]) 42 | } 43 | func BenchmarkZSorterUint64T(b *testing.B) { 44 | testSortBencher[uint64](b, testTinySize, newSorter[uint64]().withCutoff(0).Sort) 45 | } 46 | 47 | // small 48 | func BenchmarkSortSortUint64S(b *testing.B) { 49 | testSortBencher[uint64](b, testSmallSize, sortSort[uint64]) 50 | } 51 | func BenchmarkSlicesSortUint64S(b *testing.B) { 52 | testSortBencher[uint64](b, testSmallSize, slices.Sort[[]uint64, uint64]) 53 | } 54 | func BenchmarkZSortUint64S(b *testing.B) { 55 | testSortBencher[uint64](b, testSmallSize, Sort[uint64]) 56 | } 57 | func BenchmarkZSorterUint64S(b *testing.B) { 58 | testSortBencher[uint64](b, testSmallSize, newSorter[uint64]().withCutoff(0).Sort) 59 | } 60 | 61 | // medium 62 | func BenchmarkSortSortUint64M(b *testing.B) { 63 | testSortBencher[uint64](b, testMediumSize, sortSort[uint64]) 64 | } 65 | func BenchmarkSlicesSortUint64M(b *testing.B) { 66 | testSortBencher[uint64](b, testMediumSize, slices.Sort[[]uint64, uint64]) 67 | } 68 | func BenchmarkZSortUint64M(b *testing.B) { 69 | testSortBencher[uint64](b, testMediumSize, Sort[uint64]) 70 | } 71 | func BenchmarkZSorterUint64M(b *testing.B) { 72 | testSortBencher[uint64](b, testMediumSize, newSorter[uint64]().withCutoff(0).Sort) 73 | } 74 | 75 | // large 76 | func BenchmarkSortSortUint64L(b *testing.B) { 77 | testSortBencher[uint64](b, testLargeSize, sortSort[uint64]) 78 | } 79 | func BenchmarkSlicesSortUint64L(b *testing.B) { 80 | testSortBencher[uint64](b, testLargeSize, slices.Sort[[]uint64, uint64]) 81 | } 82 | func BenchmarkZSortUint64L(b *testing.B) { 83 | testSortBencher[uint64](b, testLargeSize, Sort[uint64]) 84 | } 85 | func BenchmarkZSorterUint64L(b *testing.B) { 86 | testSortBencher[uint64](b, testLargeSize, newSorter[uint64]().withCutoff(0).Sort) 87 | } 88 | 89 | // presorted 90 | func BenchmarkSortSortSorted(b *testing.B) { 91 | testBencher[uint64](b, sortSort[uint64], 92 | sortedTestData[uint64](internal.RandInteger[uint64](), testSmallSize)) 93 | } 94 | func BenchmarkSlicesSortSorted(b *testing.B) { 95 | testBencher[uint64](b, slices.Sort[[]uint64, uint64], 96 | sortedTestData[uint64](internal.RandInteger[uint64](), testSmallSize)) 97 | } 98 | func BenchmarkZSortSorted(b *testing.B) { 99 | testBencher[uint64](b, Sort[uint64], 100 | sortedTestData[uint64](internal.RandInteger[uint64](), testSmallSize)) 101 | } 102 | func BenchmarkZSorterSorted(b *testing.B) { 103 | testBencher[uint64](b, NewSorter[uint64]().Sort, 104 | sortedTestData[uint64](internal.RandInteger[uint64](), testSmallSize)) 105 | } 106 | 107 | func testSortBencher[T Integer](b *testing.B, size int, sortFunc func([]T)) { 108 | testBencher(b, sortFunc, testDataFromRng[T](internal.RandInteger[T](), size)) 109 | } 110 | 111 | // for bench b, tests s by copying rnd to x and sorting x repeatedly 112 | func testBencher[T cmp.Ordered](b *testing.B, sortFunc func([]T), getTestData func(n int) [][]T) { 113 | b.StopTimer() 114 | rnd := getTestData(b.N) 115 | b.ResetTimer() 116 | b.StartTimer() 117 | for i := 0; i < b.N; i++ { 118 | sortFunc(rnd[i]) 119 | } 120 | } 121 | 122 | type sortable[I Integer] []I 123 | 124 | func (s sortable[I]) Len() int { return len(s) } 125 | func (s sortable[I]) Less(i, j int) bool { return s[i] < s[j] } 126 | func (s sortable[I]) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 127 | 128 | func sortSort[I Integer](x []I) { 129 | sort.Sort(sortable[I](x)) 130 | } 131 | 132 | // testDataFromRng returns a function that generates tables of test data 133 | // using the given random value generator and slice size. 134 | func testDataFromRng[T any](rng func() T, size int) func(int) [][]T { 135 | return func(n int) [][]T { 136 | result := make([][]T, n) 137 | for i := 0; i < n; i++ { 138 | result[i] = make([]T, size) 139 | internal.FillSlice(result[i], rng) 140 | } 141 | return result 142 | } 143 | } 144 | 145 | // sortedTestData creates a function that generates tables of presorted test data 146 | // using the given random value generator and slice size. 147 | func sortedTestData[T cmp.Ordered](rng func() T, size int) func(int) [][]T { 148 | return func(n int) [][]T { 149 | result := testDataFromRng[T](rng, size)(n) 150 | var wg sync.WaitGroup 151 | cpus := runtime.NumCPU() 152 | for cpu := 0; cpu < cpus; cpu++ { 153 | wg.Add(1) 154 | go func(c int) { 155 | defer wg.Done() 156 | for i := c; i < len(result); i += cpus { 157 | slices.Sort(result[i]) 158 | } 159 | }(cpu) 160 | } 161 | wg.Wait() 162 | return result 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /constraints.go: -------------------------------------------------------------------------------- 1 | package zermelo 2 | 3 | // Signed is a constraint that permits any signed integer type. 4 | type Signed interface { 5 | ~int | ~int8 | ~int16 | ~int32 | ~int64 6 | } 7 | 8 | // Unsigned is a constraint that permits any unsigned integer type. 9 | type Unsigned interface { 10 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr 11 | } 12 | 13 | // Integer is a constraint that permits any integer type. 14 | type Integer interface { 15 | Signed | Unsigned 16 | } 17 | -------------------------------------------------------------------------------- /floats/README.md: -------------------------------------------------------------------------------- 1 | zermelo/floats 2 | ============== 3 | This subpackage handles sorting float slices. 4 | 5 | Example 6 | ------- 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "github.com/shawnsmithdev/zermelo/v2/floats" 13 | "something" 14 | ) 15 | 16 | func main() { 17 | var x []float64 18 | x = something.GetFloatData() 19 | floats.SortFloats(x) 20 | } 21 | ``` 22 | 23 | Sorter 24 | ====== 25 | 26 | The `Sorter` returned by `NewFloatSorter()` will reuse buffers created during `Sort()` calls. This is not thread safe, 27 | and behaves in the same manner as `zermelo.NewSorter()`, but for float types. 28 | 29 | Sorter Example 30 | -------------- 31 | ```go 32 | package main 33 | 34 | import ( 35 | "github.com/shawnsmithdev/zermelo/v2/floats" 36 | "something" 37 | ) 38 | 39 | func main() { 40 | var x [][]float64 41 | x = something.GetFloatDatas() 42 | sorter := floats.NewFloatSorter[float64]() 43 | for _, y := range x { 44 | sorter.Sort(y) 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /floats/float_bench_test.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "cmp" 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "runtime" 7 | "slices" 8 | "sync" 9 | "testing" 10 | ) 11 | 12 | // Benchmarks 13 | const testTinySize = compSortCutoffFloat32 14 | const testSmallSize = compSortCutoffFloat64 15 | const testMediumSize = 1024 // ~1k * 64bit = 8 KB 16 | const testLargeSize = 1 << 20 // ~1M * 64bit = 8 MB 17 | 18 | // tiny32 19 | func BenchmarkSortSortF32T(b *testing.B) { 20 | testBencher[float32](b, sortSort[float32], 21 | testDataFromRng[float32](randFloat32(false), testTinySize)) 22 | } 23 | func BenchmarkZSortF32T(b *testing.B) { 24 | testBencher[float32](b, SortFloats[float32], 25 | testDataFromRng[float32](randFloat32(false), testTinySize)) 26 | } 27 | func BenchmarkZSorterF32T(b *testing.B) { 28 | testBencher[float32](b, NewFloatSorter[float32]().Sort, 29 | testDataFromRng[float32](randFloat32(false), testTinySize)) 30 | } 31 | 32 | // tiny 33 | func BenchmarkSortSortF64T(b *testing.B) { 34 | testBencher[float64](b, sortSort[float64], 35 | testDataFromRng[float64](randFloat64(false), testTinySize)) 36 | } 37 | func BenchmarkZSortF64T(b *testing.B) { 38 | testBencher[float64](b, SortFloats[float64], 39 | testDataFromRng[float64](randFloat64(false), testTinySize)) 40 | } 41 | func BenchmarkZSorterF64T(b *testing.B) { 42 | testBencher[float64](b, NewFloatSorter[float64]().Sort, 43 | testDataFromRng[float64](randFloat64(false), testTinySize)) 44 | } 45 | 46 | // small 47 | func BenchmarkSortSortF64S(b *testing.B) { 48 | testBencher[float64](b, sortSort[float64], 49 | testDataFromRng[float64](randFloat64(false), testSmallSize)) 50 | } 51 | func BenchmarkZSortF64S(b *testing.B) { 52 | testBencher[float64](b, SortFloats[float64], 53 | testDataFromRng[float64](randFloat64(false), testSmallSize)) 54 | } 55 | func BenchmarkZSorterF64S(b *testing.B) { 56 | testBencher[float64](b, NewFloatSorter[float64]().Sort, 57 | testDataFromRng[float64](randFloat64(false), testSmallSize)) 58 | } 59 | 60 | // medium 61 | func BenchmarkSortSortF64M(b *testing.B) { 62 | testBencher[float64](b, sortSort[float64], 63 | testDataFromRng[float64](randFloat64(false), testMediumSize)) 64 | } 65 | func BenchmarkZSortF64M(b *testing.B) { 66 | testBencher[float64](b, SortFloats[float64], 67 | testDataFromRng[float64](randFloat64(false), testMediumSize)) 68 | } 69 | func BenchmarkZSorterF64M(b *testing.B) { 70 | testBencher[float64](b, NewFloatSorter[float64]().Sort, 71 | testDataFromRng[float64](randFloat64(false), testMediumSize)) 72 | } 73 | 74 | // large 75 | func BenchmarkSortSortF64L(b *testing.B) { 76 | testBencher[float64](b, sortSort[float64], 77 | testDataFromRng[float64](randFloat64(false), testLargeSize)) 78 | } 79 | func BenchmarkZSortF64L(b *testing.B) { 80 | testBencher[float64](b, SortFloats[float64], 81 | testDataFromRng[float64](randFloat64(false), testLargeSize)) 82 | } 83 | func BenchmarkZSorterF64L(b *testing.B) { 84 | testBencher[float64](b, NewFloatSorter[float64]().Sort, 85 | testDataFromRng[float64](randFloat64(false), testLargeSize)) 86 | } 87 | 88 | // presorted 89 | func BenchmarkSortSortSorted(b *testing.B) { 90 | testBencher[float64](b, sortSort[float64], 91 | sortedTestData[float64](randFloat64(false), testSmallSize)) 92 | } 93 | func BenchmarkZSortSorted(b *testing.B) { 94 | testBencher[float64](b, SortFloats[float64], 95 | sortedTestData[float64](randFloat64(false), testSmallSize)) 96 | } 97 | func BenchmarkZSorterSorted(b *testing.B) { 98 | testBencher[float64](b, NewFloatSorter[float64]().Sort, 99 | sortedTestData[float64](randFloat64(false), testSmallSize)) 100 | } 101 | 102 | // for bench b, tests s by copying rnd to x and sorting x repeatedly 103 | func testBencher[T any](b *testing.B, sortFunc func([]T), getTestData func(n int) [][]T) { 104 | b.StopTimer() 105 | rnd := getTestData(b.N) 106 | b.ResetTimer() 107 | b.StartTimer() 108 | for i := 0; i < b.N; i++ { 109 | sortFunc(rnd[i]) 110 | } 111 | } 112 | 113 | // testDataFromRng returns a function that generates tables of test data 114 | // using the given random value generator and slice size. 115 | func testDataFromRng[T any](rng func() T, size int) func(int) [][]T { 116 | return func(n int) [][]T { 117 | result := make([][]T, n) 118 | for i := 0; i < n; i++ { 119 | result[i] = make([]T, size) 120 | internal.FillSlice(result[i], rng) 121 | } 122 | return result 123 | } 124 | } 125 | 126 | // sortedTestData creates a function that generates tables of presorted test data 127 | // using the given random value generator and slice size. 128 | func sortedTestData[T cmp.Ordered](rng func() T, size int) func(int) [][]T { 129 | return func(n int) [][]T { 130 | result := testDataFromRng[T](rng, size)(n) 131 | var wg sync.WaitGroup 132 | cpus := runtime.NumCPU() 133 | for cpu := 0; cpu < cpus; cpu++ { 134 | wg.Add(1) 135 | go func(c int) { 136 | defer wg.Done() 137 | for i := c; i < len(result); i += cpus { 138 | slices.Sort(result[i]) 139 | } 140 | }(cpu) 141 | } 142 | wg.Wait() 143 | return result 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /floats/float_sorter.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2" 5 | "slices" 6 | ) 7 | 8 | // cutoffSorter is a Sorter with adjustable comparison sort cutoff, for testing. 9 | type cutoffSorter[F Float] interface { 10 | zermelo.Sorter[F] 11 | withCutoff(int) cutoffSorter[F] 12 | } 13 | 14 | type floatSorter[F Float, U zermelo.Unsigned] struct { 15 | uintSorter zermelo.Sorter[U] 16 | compSortCutoff int 17 | topBit U 18 | } 19 | 20 | func (s *floatSorter[F, U]) Sort(x []F) { 21 | x = sortNaNs(x) 22 | if len(x) < 2 { 23 | return 24 | } 25 | if len(x) < s.compSortCutoff { 26 | slices.Sort(x) 27 | return 28 | } 29 | 30 | y := unsafeSliceConvert[F, U](x) 31 | floatFlip[U](y, s.topBit) 32 | s.uintSorter.Sort(y) 33 | floatUnflip[U](y, s.topBit) 34 | } 35 | 36 | func (s *floatSorter[F, U]) withCutoff(cutoff int) cutoffSorter[F] { 37 | s.compSortCutoff = cutoff 38 | return s 39 | } 40 | 41 | // NewFloatSorter creates a new Sorter for float slices that will use radix sort on large slices and reuses buffers. 42 | // The first sort creates a buffer the same size as the slice being sorted and keeps it for future use. 43 | // Later sorts may grow this buffer as needed. The FloatSorter returned is not thread safe. 44 | // Using this sorter can be much faster than repeat calls to SortFloats. 45 | func NewFloatSorter[F Float]() zermelo.Sorter[F] { 46 | return newFloatSorter[F]() 47 | } 48 | 49 | func newFloatSorter[F Float]() cutoffSorter[F] { 50 | if isFloat32[F]() { 51 | return &floatSorter[F, uint32]{ 52 | uintSorter: zermelo.NewSorter[uint32](), 53 | compSortCutoff: compSortCutoffFloat32, 54 | topBit: uint32(1) << 31, 55 | } 56 | } 57 | return &floatSorter[F, uint64]{ 58 | uintSorter: zermelo.NewSorter[uint64](), 59 | compSortCutoff: compSortCutoffFloat64, 60 | topBit: uint64(1) << 63, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /floats/float_sorter_test.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2" 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "math" 7 | "slices" 8 | "testing" 9 | ) 10 | 11 | func TestSorter(t *testing.T) { 12 | if !testSorter[float32](randFloat32(false), false, false) { 13 | t.Fatal("failed float32") 14 | } 15 | if !testSorter[float64](randFloat64(false), false, false) { 16 | t.Fatal("failed float64") 17 | } 18 | if !testSorter[float32](randFloat32(false), false, true) { 19 | t.Fatal("failed float32 cutoff") 20 | } 21 | if !testSorter[float64](randFloat64(false), false, true) { 22 | t.Fatal("failed float64 cutoff") 23 | } 24 | } 25 | 26 | func TestSorterNaNs(t *testing.T) { 27 | if !testSorter[float32](randFloat32(true), true, false) { 28 | t.Fatal("failed float32 nans") 29 | } 30 | if !testSorter[float64](randFloat64(true), true, false) { 31 | t.Fatal("failed float64 nans") 32 | } 33 | if !testSorter[float32](randFloat32(true), true, true) { 34 | t.Fatal("failed float32 nans cutoff") 35 | } 36 | if !testSorter[float64](randFloat64(true), true, true) { 37 | t.Fatal("failed float64 nans cutoff") 38 | } 39 | } 40 | func TestSorterOnlyNaNs(t *testing.T) { 41 | if !testSorter[float32](func() float32 { return float32(math.NaN()) }, true, false) { 42 | t.Fatal("failed float32 onlynans") 43 | } 44 | if !testSorter[float64](math.NaN, true, false) { 45 | t.Fatal("failed float64 onlynans") 46 | } 47 | if !testSorter[float32](func() float32 { return float32(math.NaN()) }, true, true) { 48 | t.Fatal("failed float32 onlynans cutoff") 49 | } 50 | if !testSorter[float64](math.NaN, true, true) { 51 | t.Fatal("failed float64 onlynans cutoff") 52 | } 53 | } 54 | 55 | func testSorter[F Float](gen func() F, nans bool, cutoff bool) bool { 56 | var test zermelo.Sorter[F] 57 | if cutoff { 58 | test = NewFloatSorter[F]() 59 | } else { 60 | test = newFloatSorter[F]().withCutoff(0) 61 | } 62 | toTest := make([]F, testSize) 63 | for i := 0; i < testSize; i++ { 64 | internal.FillSlice(toTest[:i], gen) 65 | control := slices.Clone(toTest[:i]) 66 | if nans { 67 | sortSort[F](control) 68 | } else { 69 | slices.Sort(control) 70 | } 71 | copied := slices.Clone(toTest[:i]) 72 | test.Sort(copied) 73 | if !floatSlicesEqual[F](copied, control) { 74 | return false 75 | } 76 | test.Sort(toTest[:i]) 77 | if !floatSlicesEqual[F](toTest[:i], control) { 78 | return false 79 | } 80 | } 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /floats/floats.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "math" 5 | "runtime" 6 | "slices" 7 | ) 8 | 9 | const ( 10 | compSortCutoffFloat32 = 128 11 | compSortCutoffFloat64 = 384 12 | ) 13 | 14 | // Float is a constraint that permits any floating-point type. 15 | type Float interface { 16 | ~float32 | ~float64 17 | } 18 | 19 | // SortFloats sorts float slices. If the slice is large enough, radix sort is used by allocating a new buffer. 20 | func SortFloats[F Float](x []F) { 21 | x = sortNaNs(x) 22 | if len(x) < 2 { 23 | return 24 | } 25 | is32 := isFloat32[F]() 26 | if len(x) < compSortCutoffFloat32 || (!is32 && len(x) < compSortCutoffFloat64) { 27 | slices.Sort(x) 28 | return 29 | } 30 | sortFloatsBYOB(x, make([]F, len(x)), is32) 31 | } 32 | 33 | // SortFloatsBYOB sorts float slices with radix sort using the provided buffer. 34 | // len(buffer) must be greater or equal to len(x). 35 | func SortFloatsBYOB[F Float](x, buffer []F) { 36 | x = sortNaNs(x) 37 | if len(x) >= 2 { 38 | sortFloatsBYOB(x, buffer, isFloat32[F]()) 39 | } 40 | } 41 | 42 | func sortFloatsBYOB[F Float](x, buf []F, is32 bool) { 43 | if is32 { 44 | unsafeFlipSortFlip[F, uint32](x, buf, 32) 45 | } else { 46 | unsafeFlipSortFlip[F, uint64](x, buf, 64) 47 | } 48 | runtime.KeepAlive(buf) // avoid gc as buf is never used directly 49 | } 50 | 51 | // isFloat32 returns true if F is float32, false if float64 52 | func isFloat32[F Float]() bool { 53 | return F(math.SmallestNonzeroFloat32)/2 == 0 54 | } 55 | 56 | // isNaN returns true only if x is a float32 or float64 representing a NaN value, as only NaN is not equal itself. 57 | func isNaN[C comparable](x C) bool { return x != x } 58 | 59 | // sortNaNs put nans up front, similar to sort.Float64s, returning a slice of x excluding those nans 60 | func sortNaNs[F Float](x []F) []F { 61 | nans := 0 62 | for idx, val := range x { 63 | if isNaN(val) { 64 | x[idx] = x[nans] 65 | x[nans] = val 66 | nans++ 67 | } 68 | } 69 | return x[nans:] 70 | } 71 | -------------------------------------------------------------------------------- /floats/floats_test.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2" 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "math" 7 | "slices" 8 | "sort" 9 | "testing" 10 | ) 11 | 12 | const ( 13 | testSize = 2 * compSortCutoffFloat64 14 | ) 15 | 16 | func TestSort(t *testing.T) { 17 | testSort[float32](t, randFloat32(false), false, false) 18 | testSort[float64](t, randFloat64(false), false, false) 19 | } 20 | 21 | func TestSortNaNs(t *testing.T) { 22 | testSort[float32](t, randFloat32(true), false, true) 23 | testSort[float64](t, randFloat64(true), false, true) 24 | } 25 | 26 | func TestSortBYOB(t *testing.T) { 27 | testSort[float32](t, randFloat32(false), true, false) 28 | testSort[float32](t, randFloat32(false), true, false) 29 | } 30 | 31 | func TestSortNaNsBYOB(t *testing.T) { 32 | testSort[float32](t, randFloat32(true), true, true) 33 | testSort[float32](t, randFloat32(true), true, true) 34 | } 35 | 36 | func testSort[N Float](t *testing.T, rng func() N, byob, nans bool) { 37 | var buf []N 38 | if byob { 39 | buf = make([]N, testSize) 40 | } 41 | for i := 0; i < testSize; i++ { 42 | toTest := make([]N, i) 43 | internal.FillSlice(toTest, rng) 44 | control := slices.Clone(toTest) 45 | if nans { 46 | sortSort[N](control) 47 | } else { 48 | slices.Sort(control) 49 | } 50 | if byob { 51 | SortFloatsBYOB(toTest, buf[:i]) 52 | } else { 53 | SortFloats(toTest) 54 | } 55 | if !floatSlicesEqual(control, toTest) { 56 | t.Fatal(control, toTest) 57 | } 58 | } 59 | } 60 | 61 | // returns a function that returns random float32s 62 | func randFloat32(nans bool) func() float32 { 63 | return randFloat[float32, uint32](math.Float32frombits, nans) 64 | } 65 | 66 | // returns a function that returns random float64s 67 | func randFloat64(nans bool) func() float64 { 68 | return randFloat[float64, uint64](math.Float64frombits, nans) 69 | } 70 | 71 | // randFloat returns a function that returns random floats 72 | func randFloat[F Float, U zermelo.Unsigned](fromBits func(U) F, nans bool) func() F { 73 | rng := internal.RandInteger[U]() 74 | return func() F { 75 | for { 76 | if result := fromBits(rng()); nans || !isNaN(result) { 77 | return result 78 | } 79 | } 80 | } 81 | } 82 | 83 | type sortable[F Float] []F 84 | 85 | func (s sortable[F]) Len() int { return len(s) } 86 | func (s sortable[F]) Less(i, j int) bool { return s[i] < s[j] || (isNaN(s[i]) && !isNaN(s[j])) } 87 | func (s sortable[F]) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 88 | 89 | func sortSort[F Float](x []F) { 90 | sort.Sort(sortable[F](x)) 91 | } 92 | 93 | func floatSlicesEqual[F Float](x, y []F) bool { 94 | if len(x) != len(y) { 95 | return false 96 | } 97 | for i := range x { 98 | if x[i] != y[i] && !(isNaN(x[i]) && isNaN(y[i])) { 99 | return false 100 | } 101 | } 102 | return true 103 | } 104 | -------------------------------------------------------------------------------- /floats/floats_unsafe.go: -------------------------------------------------------------------------------- 1 | package floats 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2" 5 | "unsafe" 6 | ) 7 | 8 | // unsafeFlipSortFlip converts float slices to unsigned, flips some bits to allow sorting, sorts and unflips. 9 | // F and U must be the same bit size, and len(buf) must be >= len(x) 10 | // This will not work if NaNs are present in x. Remove them first. 11 | func unsafeFlipSortFlip[F Float, U zermelo.Unsigned](x, b []F, size uint) { 12 | xu := unsafeSliceConvert[F, U](x) 13 | bu := unsafeSliceConvert[F, U](b) 14 | floatFlip[U](xu, U(1)<<(size-1)) 15 | zermelo.SortBYOB(xu, bu) 16 | floatUnflip[U](xu, U(1)<<(size-1)) 17 | } 18 | 19 | func floatFlip[U zermelo.Unsigned](y []U, topBit U) { 20 | for idx, val := range y { 21 | if val&topBit == topBit { 22 | y[idx] = val ^ (^U(0)) 23 | } else { 24 | y[idx] = val ^ topBit 25 | } 26 | } 27 | } 28 | 29 | func floatUnflip[U zermelo.Unsigned](y []U, topBit U) { 30 | for idx, val := range y { 31 | if val&topBit == topBit { 32 | y[idx] = val ^ topBit 33 | } else { 34 | y[idx] = val ^ (^U(0)) 35 | } 36 | } 37 | } 38 | 39 | // unsafeSliceConvert takes a slice of one type and returns a slice of another type using the same memory 40 | // for the backing array. F and U obviously must be exactly the same size for this to work. 41 | // 42 | // This must only be used to temporarily treat elements in a slice as though they were of a different type. 43 | // One must not modify the length or capacity of either the given or returned slice 44 | // while the returned slice is still in scope. 45 | // 46 | // If x goes out of scope, the returned slice becomes invalid, as they share memory but the garbage collector is 47 | // unaware of the returned slice and may invalidate that memory. Working around this may require 48 | // use of `runtime.KeepAlive(x)`. 49 | func unsafeSliceConvert[F any, U any](x []F) []U { 50 | uPointer := (*U)(unsafe.Pointer(unsafe.SliceData(x))) 51 | return unsafe.Slice(uPointer, cap(x))[:len(x)] 52 | } 53 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shawnsmithdev/zermelo/v2 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= 2 | golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 3 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "crypto/rand" 5 | ) 6 | 7 | const maxSize uint = 64 8 | 9 | // Integer is a constraint that permits any integer type. 10 | type Integer interface { 11 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | ~int | ~int8 | ~int16 | ~int32 | ~int64 12 | } 13 | 14 | // Detect returns bit size and min value of T 15 | func Detect[T Integer]() (uint, T) { 16 | // 'fffe' has all but least bit set 17 | fffe := (^T(0)) ^ T(1) 18 | 19 | // find size of T in bits 20 | size := maxSize 21 | for fffe<<(size>>1) == 0 { 22 | size = size >> 1 23 | } 24 | 25 | // if 'ffff' is positive, T is unsigned 26 | if ^T(0) > 0 { 27 | return size, 0 28 | } 29 | // T is signed, min val is '8000' 30 | return size, fffe << (size - 2) 31 | } 32 | 33 | // FillSlice will fill a slice with values returned by gen. 34 | func FillSlice[T any](x []T, gen func() T) { 35 | for i := range x { 36 | x[i] = gen() 37 | } 38 | } 39 | 40 | // RandInteger returns a function that generates random integers, including negative values. 41 | func RandInteger[I Integer]() func() I { 42 | tSize, _ := Detect[I]() 43 | buf := make([]byte, tSize/8) 44 | return func() I { 45 | var result I 46 | _, _ = rand.Read(buf) 47 | for i, val := range buf { 48 | result |= I(val) << (i * 8) 49 | } 50 | return result 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/internal_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const intSize uint = 1 << (5 + (^uint(0))>>32&1) 11 | 12 | func TestDetect(t *testing.T) { 13 | testDetect[uint](t, intSize, 0) 14 | testDetect[uint8](t, 8, 0) 15 | testDetect[uint16](t, 16, 0) 16 | testDetect[uint32](t, 32, 0) 17 | testDetect[uint64](t, 64, 0) 18 | testDetect[int](t, intSize, math.MinInt) 19 | testDetect[int8](t, 8, math.MinInt8) 20 | testDetect[int16](t, 16, math.MinInt16) 21 | testDetect[int32](t, 32, math.MinInt32) 22 | testDetect[int64](t, 64, math.MinInt64) 23 | } 24 | 25 | func testDetect[I Integer](t *testing.T, size uint, min I) { 26 | start := time.Now() 27 | detectedSize, detectedMin := Detect[I]() 28 | delta := time.Now().Sub(start) 29 | if size != detectedSize { 30 | t.Fatalf("%T: Wrong size, expected %v, got %v", I(0), size, detectedSize) 31 | } 32 | if detectedMin != min { 33 | t.Fatalf("%T: Wrong min, expected %v, got %v", I(0), min, detectedMin) 34 | } 35 | t.Logf("%T: detect in %v", I(0), delta) 36 | } 37 | 38 | func TestFillSlice(t *testing.T) { 39 | testFillSlice(t, "test") 40 | testFillSlice(t, "foo") 41 | testFillSlice(t, "bar") 42 | } 43 | 44 | func testFillSlice(t *testing.T, toFill string) { 45 | test := make([]string, 128) 46 | n := 0 47 | FillSlice(test, func() string { 48 | result := fmt.Sprintf("%s%d", toFill, n) 49 | n++ 50 | return result 51 | }) 52 | for i, val := range test { 53 | if val != fmt.Sprintf("%s%d", toFill, i) { 54 | t.Fatal("wrong value filled") 55 | } 56 | } 57 | } 58 | 59 | func TestRandInteger(t *testing.T) { 60 | t.Log(time.Now()) // tests are cached and that can be confusing 61 | // just print results so it can be checked by a human 62 | randIntPrint(t, RandInteger[uint]()()) 63 | randIntPrint(t, RandInteger[uint8]()()) 64 | randIntPrint(t, RandInteger[uint16]()()) 65 | randIntPrint(t, RandInteger[uint32]()()) 66 | randIntPrint(t, RandInteger[uintptr]()()) 67 | randIntPrint(t, RandInteger[int]()()) 68 | randIntPrint(t, RandInteger[int8]()()) 69 | randIntPrint(t, RandInteger[int16]()()) 70 | randIntPrint(t, RandInteger[int32]()()) 71 | randIntPrint(t, RandInteger[int64]()()) 72 | } 73 | 74 | func randIntPrint(t *testing.T, x any) { 75 | t.Logf("RandInteger[%T] %v", x, x) 76 | } 77 | -------------------------------------------------------------------------------- /sorter.go: -------------------------------------------------------------------------------- 1 | package zermelo 2 | 3 | import ( 4 | "cmp" 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "slices" 7 | ) 8 | 9 | // Sorter describes types that can sort slices. 10 | type Sorter[T cmp.Ordered] interface { 11 | // Sort sorts slices in ascending order. 12 | Sort(x []T) 13 | } 14 | 15 | // cutoffSorter is a Sorter with adjustable comparison sort cutoff, for testing. 16 | type cutoffSorter[T Integer] interface { 17 | Sorter[T] 18 | withCutoff(int) cutoffSorter[T] 19 | } 20 | 21 | type sorter[I Integer] struct { 22 | buf []I 23 | compSortCutoff int 24 | minval I 25 | size uint 26 | } 27 | 28 | func (s *sorter[I]) Sort(x []I) { 29 | if len(x) < s.compSortCutoff { 30 | slices.Sort(x) 31 | return 32 | } 33 | if len(s.buf) < len(x) { 34 | s.buf = make([]I, allocSize(len(s.buf), len(x))) 35 | } 36 | sortBYOB(x, s.buf, s.size, s.minval) 37 | } 38 | 39 | func (s *sorter[I]) withCutoff(cutoff int) cutoffSorter[I] { 40 | s.compSortCutoff = cutoff 41 | return s 42 | } 43 | 44 | // NewSorter creates a new Sorter that will use radix sort on large slices and reuses buffers. 45 | // The first sort creates a buffer the same size as the slice being sorted and keeps it for future use. 46 | // Later sorts may grow this buffer as needed. The Sorter returned is not thread safe. 47 | // Using this sorter can be much faster than repeat calls to Sort. 48 | func NewSorter[I Integer]() Sorter[I] { 49 | return newSorter[I]() 50 | } 51 | 52 | func newSorter[I Integer]() cutoffSorter[I] { 53 | size, minval := internal.Detect[I]() 54 | cutoff := compSortCutoff 55 | if size == 64 { 56 | cutoff = compSortCutoff64 57 | } 58 | return &sorter[I]{ 59 | compSortCutoff: cutoff, 60 | minval: minval, 61 | size: size, 62 | } 63 | } 64 | 65 | // Given an existing buffer capacity and a requested one, finds a new buffer size. 66 | // For the first alloc this will equal requested size, then after at it leaves 67 | // a 25% buffer for future growth. 68 | func allocSize(bufCap, reqLen int) int { 69 | if bufCap == 0 { 70 | return reqLen 71 | } 72 | return 5 * reqLen / 4 73 | } 74 | -------------------------------------------------------------------------------- /sorter_test.go: -------------------------------------------------------------------------------- 1 | package zermelo 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2/internal" 5 | "slices" 6 | "testing" 7 | ) 8 | 9 | func TestSorter(t *testing.T) { 10 | testSorter[int8](t, internal.RandInteger[int8](), false) 11 | testSorter[int8](t, internal.RandInteger[int8](), true) 12 | testSorter[int16](t, internal.RandInteger[int16](), false) 13 | testSorter[int16](t, internal.RandInteger[int16](), true) 14 | testSorter[int32](t, internal.RandInteger[int32](), false) 15 | testSorter[int32](t, internal.RandInteger[int32](), true) 16 | testSorter[int64](t, internal.RandInteger[int64](), false) 17 | testSorter[int64](t, internal.RandInteger[int64](), true) 18 | testSorter[int](t, internal.RandInteger[int](), false) 19 | testSorter[int](t, internal.RandInteger[int](), true) 20 | testSorter[uint8](t, internal.RandInteger[uint8](), false) 21 | testSorter[uint8](t, internal.RandInteger[uint8](), true) 22 | testSorter[byte](t, internal.RandInteger[byte](), false) 23 | testSorter[byte](t, internal.RandInteger[byte](), true) 24 | testSorter[uint16](t, internal.RandInteger[uint16](), false) 25 | testSorter[uint16](t, internal.RandInteger[uint16](), true) 26 | testSorter[uint32](t, internal.RandInteger[uint32](), false) 27 | testSorter[uint32](t, internal.RandInteger[uint32](), true) 28 | testSorter[uint64](t, internal.RandInteger[uint64](), false) 29 | testSorter[uint64](t, internal.RandInteger[uint64](), true) 30 | testSorter[uintptr](t, internal.RandInteger[uintptr](), false) 31 | testSorter[uintptr](t, internal.RandInteger[uintptr](), true) 32 | testSorter[uint](t, internal.RandInteger[uint](), false) 33 | testSorter[uint](t, internal.RandInteger[uint](), true) 34 | } 35 | 36 | func testSorter[I Integer](t *testing.T, gen func() I, cutoff bool) { 37 | var test Sorter[I] 38 | if cutoff { 39 | test = NewSorter[I]() 40 | } else { 41 | test = newSorter[I]().withCutoff(0) 42 | } 43 | toTest := make([]I, testSize) 44 | for i := 0; i < testSize; i++ { 45 | internal.FillSlice(toTest[:i], gen) 46 | control := slices.Clone(toTest[:i]) 47 | slices.Sort(control) 48 | copied := slices.Clone(toTest[:i]) 49 | test.Sort(copied) 50 | if !slices.Equal(copied, control) { 51 | t.Fatal("cutoff=", cutoff, ", copied != control", copied, control) 52 | } 53 | test.Sort(toTest[:i]) 54 | if !slices.Equal(toTest[:i], control) { 55 | t.Fatal("cutoff=", cutoff, "toTest ! control", toTest, control) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /zermelo.go: -------------------------------------------------------------------------------- 1 | // Package zermelo is a library for sorting slices in Go. 2 | package zermelo // import "github.com/shawnsmithdev/zermelo/v2" 3 | 4 | import ( 5 | "github.com/shawnsmithdev/zermelo/v2/internal" 6 | "slices" 7 | ) 8 | 9 | const ( 10 | radix uint = 8 11 | compSortCutoff64 = 256 12 | compSortCutoff = 128 13 | ) 14 | 15 | // Sort sorts integer slices. If the slice is large enough, radix sort is used by allocating a new buffer. 16 | func Sort[T Integer](x []T) { 17 | if len(x) < 2 { 18 | return 19 | } 20 | size, minval := internal.Detect[T]() 21 | if len(x) < compSortCutoff || (size == 64 && len(x) < compSortCutoff64) { 22 | slices.Sort(x) 23 | } else { 24 | sortBYOB(x, make([]T, len(x)), size, minval) 25 | } 26 | } 27 | 28 | // SortBYOB sorts integer slices with radix sort using the provided buffer. 29 | // len(buffer) must be greater or equal to len(x). 30 | func SortBYOB[T Integer](x, buffer []T) { 31 | if len(x) >= 2 { 32 | size, minval := internal.Detect[T]() 33 | sortBYOB(x, buffer, size, minval) 34 | } 35 | } 36 | 37 | func sortBYOB[T Integer](x, buffer []T, size uint, minval T) { 38 | from := x 39 | to := buffer[:len(x)] 40 | 41 | var keyOffset uint 42 | for keyOffset = 0; keyOffset < size; keyOffset += radix { 43 | var ( 44 | offset [256]int // Keep track of where room is made for byte groups in the buffer 45 | prev = minval 46 | key uint8 47 | sorted = true 48 | ) 49 | 50 | for _, elem := range from { 51 | // For each elem to sort, fetch the byte at current radix 52 | key = uint8(elem >> keyOffset) 53 | // inc count of bytes of this type 54 | offset[key]++ 55 | if sorted { // Detect sorted 56 | sorted = elem >= prev 57 | prev = elem 58 | } 59 | } 60 | 61 | if sorted { // Short-circuit sorted 62 | break 63 | } 64 | 65 | // Find target bucket offsets 66 | var watermark int 67 | if minval != 0 && keyOffset == size-radix { 68 | // Handle signed values 69 | // Negatives 70 | for i := 128; i < len(offset); i++ { 71 | count := offset[i] 72 | offset[i] = watermark 73 | watermark += count 74 | } 75 | // Positives 76 | for i := 0; i < 128; i++ { 77 | count := offset[i] 78 | offset[i] = watermark 79 | watermark += count 80 | } 81 | } else { 82 | for i, count := range offset { 83 | offset[i] = watermark 84 | watermark += count 85 | } 86 | } 87 | 88 | // Swap values between the buffers by radix 89 | for _, elem := range from { 90 | key = uint8(elem >> keyOffset) // Get the byte of each element at the radix 91 | to[offset[key]] = elem // Copy the element depending on byte offsets 92 | offset[key]++ 93 | } 94 | 95 | // Reverse buffers on each pass 96 | from, to = to, from 97 | } 98 | 99 | // copy from buffer if done during odd turn 100 | if radix&keyOffset == radix { 101 | copy(to, from) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /zermelo_test.go: -------------------------------------------------------------------------------- 1 | package zermelo 2 | 3 | import ( 4 | "github.com/shawnsmithdev/zermelo/v2/internal" 5 | "slices" 6 | "testing" 7 | ) 8 | 9 | const ( 10 | // Const int size thanks to kostya-sh@github 11 | intSize uint = 1 << (5 + (^uint(0))>>32&1) 12 | testSize = 2 * compSortCutoff64 13 | ) 14 | 15 | func TestSort(t *testing.T) { 16 | testSort[int8](t, internal.RandInteger[int8](), false) 17 | testSort[int16](t, internal.RandInteger[int16](), false) 18 | testSort[int32](t, internal.RandInteger[int32](), false) 19 | testSort[int64](t, internal.RandInteger[int64](), false) 20 | testSort[int](t, internal.RandInteger[int](), false) 21 | testSort[uint8](t, internal.RandInteger[uint8](), false) 22 | testSort[uint16](t, internal.RandInteger[uint16](), false) 23 | testSort[uint32](t, internal.RandInteger[uint32](), false) 24 | testSort[uint64](t, internal.RandInteger[uint64](), false) 25 | testSort[uintptr](t, internal.RandInteger[uintptr](), false) 26 | testSort[uint](t, internal.RandInteger[uint](), false) 27 | } 28 | 29 | func TestSortBYOB(t *testing.T) { 30 | testSort[int8](t, internal.RandInteger[int8](), true) 31 | testSort[int16](t, internal.RandInteger[int16](), true) 32 | testSort[int32](t, internal.RandInteger[int32](), true) 33 | testSort[int64](t, internal.RandInteger[int64](), true) 34 | testSort[int](t, internal.RandInteger[int](), true) 35 | testSort[uint8](t, internal.RandInteger[uint8](), true) 36 | testSort[uint16](t, internal.RandInteger[uint16](), true) 37 | testSort[uint32](t, internal.RandInteger[uint32](), true) 38 | testSort[uint64](t, internal.RandInteger[uint64](), true) 39 | testSort[uintptr](t, internal.RandInteger[uintptr](), true) 40 | testSort[uint](t, internal.RandInteger[uint](), true) 41 | } 42 | 43 | func testSort[N Integer](t *testing.T, rng func() N, byob bool) { 44 | for i := 0; i <= testSize; i++ { 45 | toTest := make([]N, i) 46 | internal.FillSlice(toTest, rng) 47 | control := slices.Clone(toTest) 48 | slices.Sort(control) 49 | if byob { 50 | SortBYOB(toTest, make([]N, i)) 51 | } else { 52 | Sort(toTest) 53 | } 54 | if !slices.Equal(control, toTest) { 55 | t.Fatal(control, toTest) 56 | } 57 | } 58 | } 59 | --------------------------------------------------------------------------------