mirror of
https://github.com/jimeh/rands.git
synced 2026-02-19 11:26:38 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c98b79b4c5 | ||
|
b28e506295
|
|||
| a141938394 |
2
.github/.release-please-manifest.json
vendored
2
.github/.release-please-manifest.json
vendored
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
".": "0.4.0"
|
".": "0.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@@ -80,7 +80,6 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version:
|
go-version:
|
||||||
- "1.17"
|
|
||||||
- "1.18"
|
- "1.18"
|
||||||
- "1.19"
|
- "1.19"
|
||||||
- "1.20"
|
- "1.20"
|
||||||
@@ -88,11 +87,13 @@ jobs:
|
|||||||
- "1.22"
|
- "1.22"
|
||||||
- "1.23"
|
- "1.23"
|
||||||
- "1.24"
|
- "1.24"
|
||||||
|
- "stable"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
|
check-latest: true
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
env:
|
env:
|
||||||
|
|||||||
11
CHANGELOG.md
11
CHANGELOG.md
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||||
|
|
||||||
|
## [0.5.0](https://github.com/jimeh/rands/compare/v0.4.0...v0.5.0) (2025-03-10)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* **shuffle:** Minimum required Go version is now 1.18 due the `ShuffleSlice` using generics.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **shuffle:** add Shuffle and ShuffleSlice functions ([#11](https://github.com/jimeh/rands/issues/11)) ([a141938](https://github.com/jimeh/rands/commit/a14193839475562268ea10e356580b33b27ebedc))
|
||||||
|
|
||||||
## [0.4.0](https://github.com/jimeh/rands/compare/v0.3.0...v0.4.0) (2025-03-07)
|
## [0.4.0](https://github.com/jimeh/rands/compare/v0.3.0...v0.4.0) (2025-03-07)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -5,8 +5,8 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Go package providing a suite of functions that use <code>crypto/rand</code>
|
Go package providing a suite of functions that use <code>crypto/rand</code>
|
||||||
to generate cryptographically secure random strings in various formats, as
|
to generate cryptographically secure random data in various forms and
|
||||||
well as ints and bytes.
|
formats.
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -59,6 +59,9 @@ n, err := rands.Int(2147483647) // => 1334400235
|
|||||||
n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
n, err := rands.Int64(int64(9223372036854775807)) // => 8256935979116161233
|
||||||
|
|
||||||
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
b, err := rands.Bytes(8) // => [0 220 137 243 135 204 34 63]
|
||||||
|
|
||||||
|
err := rands.Shuffle(len(arr), func(i, j) { arr[i], arr[j] = arr[j], arr[i] })
|
||||||
|
err := rands.ShuffleSlice(arr)
|
||||||
```
|
```
|
||||||
|
|
||||||
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust) package
|
## [`randsmust`](https://pkg.go.dev/github.com/jimeh/rands/randsmust) package
|
||||||
@@ -102,6 +105,9 @@ n := randsmust.Int(2147483647) // => 1293388115
|
|||||||
n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239
|
n := randsmust.Int64(int64(9223372036854775807)) // => 6168113630900161239
|
||||||
|
|
||||||
b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51]
|
b := randsmust.Bytes(8) // => [205 128 54 95 0 95 53 51]
|
||||||
|
|
||||||
|
randsmust.Shuffle(len(arr), func(i, j) { arr[i], arr[j] = arr[j], arr[i] })
|
||||||
|
randsmust.ShuffleSlice(arr)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/jimeh/rands
|
module github.com/jimeh/rands
|
||||||
|
|
||||||
go 1.17
|
go 1.18
|
||||||
|
|
||||||
require github.com/stretchr/testify v1.10.0
|
require github.com/stretchr/testify v1.10.0
|
||||||
|
|
||||||
|
|||||||
9
go.sum
9
go.sum
@@ -1,19 +1,10 @@
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
32
randsmust/shuffle.go
Normal file
32
randsmust/shuffle.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package randsmust
|
||||||
|
|
||||||
|
import "github.com/jimeh/rands"
|
||||||
|
|
||||||
|
// Shuffle randomizes the order of a collection of n elements using
|
||||||
|
// cryptographically secure random values from crypto/rand. It implements the
|
||||||
|
// Fisher-Yates shuffle algorithm.
|
||||||
|
//
|
||||||
|
// The swap function is called to exchange values at indices i and j. This
|
||||||
|
// signature is compatible with Shuffle from math/rand and math/rand/v2 for easy
|
||||||
|
// migration.
|
||||||
|
//
|
||||||
|
// If an error occurs during shuffling, this function will panic.
|
||||||
|
func Shuffle(n int, swap func(i, j int)) {
|
||||||
|
err := rands.Shuffle(n, swap)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShuffleSlice randomizes the order of elements in a slice in-place using
|
||||||
|
// cryptographically secure random values from crypto/rand.
|
||||||
|
//
|
||||||
|
// It implements the Fisher-Yates shuffle algorithm.
|
||||||
|
//
|
||||||
|
// If an error occurs during shuffling, this function will panic.
|
||||||
|
func ShuffleSlice[T any](slice []T) {
|
||||||
|
err := rands.ShuffleSlice(slice)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
randsmust/shuffle_example_test.go
Normal file
25
randsmust/shuffle_example_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package randsmust_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jimeh/rands/randsmust"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleShuffle() {
|
||||||
|
numbers := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
randsmust.Shuffle(len(numbers), func(i, j int) {
|
||||||
|
numbers[i], numbers[j] = numbers[j], numbers[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println(numbers) // => [4 2 5 3 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleShuffleSlice() {
|
||||||
|
mixed := []any{1, "two", 3.14, true, nil}
|
||||||
|
|
||||||
|
randsmust.ShuffleSlice(mixed)
|
||||||
|
|
||||||
|
fmt.Println(mixed) // => [two <nil> true 3.14 1]
|
||||||
|
}
|
||||||
456
randsmust/shuffle_test.go
Normal file
456
randsmust/shuffle_test.go
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
package randsmust
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jimeh/rands"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func factorial(n int) int {
|
||||||
|
factorial := 1
|
||||||
|
for i := 1; i <= n; i++ {
|
||||||
|
factorial *= i
|
||||||
|
}
|
||||||
|
|
||||||
|
return factorial
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("n < 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(-1, func(_, _ int) {})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NotNil(t, p, "Expected a panic")
|
||||||
|
assert.ErrorIs(t, p.(error), rands.ErrInvalidShuffleNegativeN)
|
||||||
|
assert.ErrorIs(t, p.(error), rands.ErrShuffle)
|
||||||
|
assert.ErrorIs(t, p.(error), rands.Err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(0, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.Equal(t, 0, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 1", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(1, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.Equal(t, 0, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 2", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(2, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.Equal(t, 1, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
arr := make([]int, 100)
|
||||||
|
for i := range arr {
|
||||||
|
arr[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
arrCopy := make([]int, len(arr))
|
||||||
|
copy(arrCopy, arr)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(len(arr), func(i, j int) {
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.NotEqual(t, arrCopy, arr, "Shuffle did not change the array")
|
||||||
|
assert.ElementsMatch(t, arrCopy, arr, "Shuffle changed elements")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("swaps", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapSame := 0
|
||||||
|
swapDifferent := 0
|
||||||
|
arr := make([]int, 100)
|
||||||
|
for j := range arr {
|
||||||
|
arr[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(len(arr), func(i, j int) {
|
||||||
|
if i == j {
|
||||||
|
swapSame++
|
||||||
|
} else {
|
||||||
|
swapDifferent++
|
||||||
|
}
|
||||||
|
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||||
|
assert.Equal(t, len(arr)-1, swapSame+swapDifferent,
|
||||||
|
"Unexpected swaps count",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure we have more different-element swaps than self-swaps. The
|
||||||
|
// lower the input shuffle n value, the more likely this assertion will
|
||||||
|
// fail. For a n=100 shuffle, this is exceptionally unlikely to fail.
|
||||||
|
assert.Greater(t, swapDifferent, swapSame,
|
||||||
|
"Expected more different-element swaps than self-swaps",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("swap ranges", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
n := 32
|
||||||
|
runs := 1000
|
||||||
|
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
called := 0
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(n, func(i, j int) {
|
||||||
|
called++
|
||||||
|
|
||||||
|
// Verify indices are in bounds.
|
||||||
|
assert.True(t,
|
||||||
|
i >= 0 && i < n, "Out of bounds index i = %d", i,
|
||||||
|
)
|
||||||
|
assert.True(t,
|
||||||
|
j >= 0 && j < n, "Out of bounds index j = %d", j,
|
||||||
|
)
|
||||||
|
|
||||||
|
// For Fisher-Yates, i should be > 0 and j should be in
|
||||||
|
// range [0,i].
|
||||||
|
assert.Greater(t, i, 0, "Expected i > 0, got i=%d", i)
|
||||||
|
assert.True(t,
|
||||||
|
j >= 0 && j <= i,
|
||||||
|
"Expected j in range [0,%d], got j=%d", i, j,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||||
|
expected := n - 1
|
||||||
|
assert.Equal(t, expected, called,
|
||||||
|
"Expected %d swap calls, got %d", expected, called,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all permutations", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Use a small array of 5 elements to make it feasible to track all
|
||||||
|
// permutations.
|
||||||
|
n := 5
|
||||||
|
fact := factorial(n) // 120
|
||||||
|
runs := fact * 3000 // 360000
|
||||||
|
|
||||||
|
permCounts := make(map[string]int)
|
||||||
|
for i := 0; i < runs; i++ {
|
||||||
|
arr := make([]int, n)
|
||||||
|
for i := range arr {
|
||||||
|
arr[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(len(arr), func(i, j int) {
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// Convert the permutation to a string key and count it.
|
||||||
|
key := fmt.Sprintf("%v", arr)
|
||||||
|
permCounts[key]++
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, fact, len(permCounts),
|
||||||
|
"Expected %d different permutations", fact,
|
||||||
|
)
|
||||||
|
|
||||||
|
wantCount := float64(runs) / float64(fact)
|
||||||
|
margin := 0.15
|
||||||
|
minAcceptable := int(wantCount * (1 - margin))
|
||||||
|
maxAcceptable := int(wantCount * (1 + margin))
|
||||||
|
|
||||||
|
for perm, count := range permCounts {
|
||||||
|
assert.True(t,
|
||||||
|
count >= minAcceptable && count <= maxAcceptable,
|
||||||
|
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||||
|
perm, count, wantCount, wantCount*margin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("distribution", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Track which positions received which random indices
|
||||||
|
n := 100
|
||||||
|
posCounts := make([]map[int]int, n)
|
||||||
|
for i := range posCounts {
|
||||||
|
posCounts[i] = make(map[int]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
runs := 3000
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
Shuffle(n, func(i, j int) {
|
||||||
|
posCounts[i][j]++
|
||||||
|
})
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each position, check that it received a reasonable distribution.
|
||||||
|
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||||
|
// Calculate how many unique positions we should expect.
|
||||||
|
// Position i should receive random positions from 0 to i, and
|
||||||
|
// allow for some statistical variation.
|
||||||
|
want := int(float64(i+1) * 0.9)
|
||||||
|
assert.GreaterOrEqual(t,
|
||||||
|
len(posCounts[i]), want,
|
||||||
|
"Position %d: expected ~%d unique indices, got %d",
|
||||||
|
i, want, len(posCounts[i]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleSlice(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("empty slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{}
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.Empty(t, slice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single element", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{42}
|
||||||
|
origSlice := make([]int, len(slice))
|
||||||
|
copy(origSlice, slice)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
origSlice, slice, "Single element slice should remain unchanged",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("two elements", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{1, 2}
|
||||||
|
origSlice := make([]int, len(slice))
|
||||||
|
copy(origSlice, slice)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// With two elements, the slice might remain the same or be swapped
|
||||||
|
assert.Len(t, slice, len(origSlice))
|
||||||
|
assert.ElementsMatch(t, origSlice, slice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := make([]int, 100)
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceCopy := make([]int, len(slice))
|
||||||
|
copy(sliceCopy, slice)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
assert.NotEqual(t,
|
||||||
|
sliceCopy, slice, "ShuffleSlice did not change the slice",
|
||||||
|
)
|
||||||
|
assert.ElementsMatch(t,
|
||||||
|
sliceCopy, slice, "ShuffleSlice changed elements",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
strSlice := []string{"a", "b", "c", "d", "e"}
|
||||||
|
strCopy := make([]string, len(strSlice))
|
||||||
|
copy(strCopy, strSlice)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(strSlice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, strCopy, strSlice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testStruct struct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
structSlice := []testStruct{
|
||||||
|
{1, "one"},
|
||||||
|
{2, "two"},
|
||||||
|
{3, "three"},
|
||||||
|
{4, "four"},
|
||||||
|
}
|
||||||
|
structCopy := make([]testStruct, len(structSlice))
|
||||||
|
copy(structCopy, structSlice)
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(structSlice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
assert.ElementsMatch(t, structCopy, structSlice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all permutations", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Use a small slice of 5 elements to make it feasible to track all
|
||||||
|
// permutations.
|
||||||
|
n := 5
|
||||||
|
fact := factorial(n) // 120
|
||||||
|
runs := fact * 3000 // 360000
|
||||||
|
|
||||||
|
permCounts := make(map[string]int)
|
||||||
|
for i := 0; i < runs; i++ {
|
||||||
|
slice := make([]int, n)
|
||||||
|
for j := range slice {
|
||||||
|
slice[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// Convert the permutation to a string key and count it.
|
||||||
|
key := fmt.Sprintf("%v", slice)
|
||||||
|
permCounts[key]++
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, fact, len(permCounts),
|
||||||
|
"Expected %d different permutations", fact,
|
||||||
|
)
|
||||||
|
|
||||||
|
wantCount := float64(runs) / float64(fact)
|
||||||
|
margin := 0.15
|
||||||
|
minAcceptable := int(wantCount * (1 - margin))
|
||||||
|
maxAcceptable := int(wantCount * (1 + margin))
|
||||||
|
|
||||||
|
for perm, count := range permCounts {
|
||||||
|
assert.True(t,
|
||||||
|
count >= minAcceptable && count <= maxAcceptable,
|
||||||
|
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||||
|
perm, count, wantCount, wantCount*margin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("distribution", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Track where each original index ends up after shuffling
|
||||||
|
n := 100
|
||||||
|
// posCounts[originalPos][newPos] tracks how many times
|
||||||
|
// the element originally at position i ended up at position j
|
||||||
|
posCounts := make([]map[int]int, n)
|
||||||
|
for i := range posCounts {
|
||||||
|
posCounts[i] = make(map[int]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
runs := 3000
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
// Create a slice where the value is its original position
|
||||||
|
slice := make([]int, n)
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
p := recoverPanic(func() {
|
||||||
|
ShuffleSlice(slice)
|
||||||
|
})
|
||||||
|
require.Nil(t, p, "Did not expect a panic")
|
||||||
|
|
||||||
|
// Track where each original position ended up
|
||||||
|
for newPos, origPos := range slice {
|
||||||
|
posCounts[origPos][newPos]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each original position, check that it was distributed
|
||||||
|
// reasonably across all possible new positions
|
||||||
|
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||||
|
// Calculate how many unique positions we should expect.
|
||||||
|
// Position i should receive random positions from 0 to i, and
|
||||||
|
// allow for some statistical variation.
|
||||||
|
want := int(float64(i+1) * 0.9)
|
||||||
|
assert.GreaterOrEqual(t,
|
||||||
|
len(posCounts[i]), want,
|
||||||
|
"Original position %d: expected ~%d unique positions, got %d",
|
||||||
|
i, want, len(posCounts[i]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
51
shuffle.go
Normal file
51
shuffle.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package rands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrShuffle = fmt.Errorf("%w: shuffle", Err)
|
||||||
|
ErrInvalidShuffleNegativeN = fmt.Errorf(
|
||||||
|
"%w: n must not be negative", ErrShuffle,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shuffle randomizes the order of a collection of n elements using
|
||||||
|
// cryptographically secure random values from crypto/rand. It implements the
|
||||||
|
// Fisher-Yates shuffle algorithm.
|
||||||
|
//
|
||||||
|
// The swap function is called to exchange values at indices i and j. This
|
||||||
|
// signature is compatible with Shuffle from math/rand and math/rand/v2 for easy
|
||||||
|
// migration.
|
||||||
|
func Shuffle(n int, swap func(i, j int)) error {
|
||||||
|
if n < 0 {
|
||||||
|
return ErrInvalidShuffleNegativeN
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := n - 1; i > 0; i-- {
|
||||||
|
j, err := Int(i + 1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
swap(i, j)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShuffleSlice randomizes the order of elements in a slice in-place using
|
||||||
|
// cryptographically secure random values from crypto/rand.
|
||||||
|
//
|
||||||
|
// It implements the Fisher-Yates shuffle algorithm.
|
||||||
|
func ShuffleSlice[T any](slice []T) error {
|
||||||
|
// If the slice has one or no elements, there's nothing to shuffle.
|
||||||
|
if len(slice) < 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Shuffle(len(slice), func(i, j int) {
|
||||||
|
slice[i], slice[j] = slice[j], slice[i]
|
||||||
|
})
|
||||||
|
}
|
||||||
32
shuffle_example_test.go
Normal file
32
shuffle_example_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package rands_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/jimeh/rands"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleShuffle() {
|
||||||
|
numbers := []int{1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
err := rands.Shuffle(len(numbers), func(i, j int) {
|
||||||
|
numbers[i], numbers[j] = numbers[j], numbers[i]
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(numbers) // => [2 4 5 1 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleShuffleSlice() {
|
||||||
|
mixed := []any{1, "two", 3.14, true, nil}
|
||||||
|
|
||||||
|
err := rands.ShuffleSlice(mixed)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(mixed) // => [3.14 true 1 two <nil>]
|
||||||
|
}
|
||||||
444
shuffle_test.go
Normal file
444
shuffle_test.go
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
package rands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func factorial(n int) int {
|
||||||
|
factorial := 1
|
||||||
|
for i := 1; i <= n; i++ {
|
||||||
|
factorial *= i
|
||||||
|
}
|
||||||
|
|
||||||
|
return factorial
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("n < 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
err := Shuffle(-1, func(_, _ int) {})
|
||||||
|
|
||||||
|
require.ErrorIs(t, err, ErrInvalidShuffleNegativeN)
|
||||||
|
require.ErrorIs(t, err, ErrShuffle)
|
||||||
|
require.ErrorIs(t, err, Err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 0", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
err := Shuffle(0, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 1", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
err := Shuffle(1, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("n == 2", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapCount := 0
|
||||||
|
err := Shuffle(2, func(_, _ int) {
|
||||||
|
swapCount++
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 1, swapCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
arr := make([]int, 100)
|
||||||
|
for i := range arr {
|
||||||
|
arr[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
arrCopy := make([]int, len(arr))
|
||||||
|
copy(arrCopy, arr)
|
||||||
|
|
||||||
|
err := Shuffle(len(arr), func(i, j int) {
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Shuffle returned an error")
|
||||||
|
|
||||||
|
assert.NotEqual(t, arrCopy, arr, "Shuffle did not change the array")
|
||||||
|
assert.ElementsMatch(t, arrCopy, arr, "Shuffle changed elements")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("swaps", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
swapSame := 0
|
||||||
|
swapDifferent := 0
|
||||||
|
arr := make([]int, 100)
|
||||||
|
for j := range arr {
|
||||||
|
arr[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Shuffle(len(arr), func(i, j int) {
|
||||||
|
if i == j {
|
||||||
|
swapSame++
|
||||||
|
} else {
|
||||||
|
swapDifferent++
|
||||||
|
}
|
||||||
|
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Shuffle returned an error")
|
||||||
|
|
||||||
|
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||||
|
assert.Equal(t, len(arr)-1, swapSame+swapDifferent,
|
||||||
|
"Unexpected swaps count",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Ensure we have more different-element swaps than self-swaps. The
|
||||||
|
// lower the input shuffle n value, the more likely this assertion will
|
||||||
|
// fail. For a n=100 shuffle, this is exceptionally unlikely to fail.
|
||||||
|
assert.Greater(t, swapDifferent, swapSame,
|
||||||
|
"Expected more different-element swaps than self-swaps",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("swap ranges", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
n := 32
|
||||||
|
runs := 1000
|
||||||
|
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
called := 0
|
||||||
|
err := Shuffle(n, func(i, j int) {
|
||||||
|
called++
|
||||||
|
|
||||||
|
// Verify indices are in bounds.
|
||||||
|
assert.True(t, i >= 0 && i < n, "Out of bounds index i = %d", i)
|
||||||
|
assert.True(t, j >= 0 && j < n, "Out of bounds index j = %d", j)
|
||||||
|
|
||||||
|
// For Fisher-Yates, i should be > 0 and j should be in range
|
||||||
|
// [0,i].
|
||||||
|
assert.Greater(t, i, 0, "Expected i > 0, got i=%d", i)
|
||||||
|
assert.True(t,
|
||||||
|
j >= 0 && j <= i,
|
||||||
|
"Expected j in range [0,%d], got j=%d", i, j,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Shuffle returned an error")
|
||||||
|
|
||||||
|
// Fisher-Yates with n elements should make exactly n-1 swaps
|
||||||
|
expected := n - 1
|
||||||
|
assert.Equal(t, expected, called,
|
||||||
|
"Expected %d swap calls, got %d", expected, called,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all permutations", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Use a small array of 5 elements to make it feasible to track all
|
||||||
|
// permutations.
|
||||||
|
n := 5
|
||||||
|
fact := factorial(n) // 120
|
||||||
|
runs := fact * 3000 // 360000
|
||||||
|
|
||||||
|
permCounts := make(map[string]int)
|
||||||
|
for i := 0; i < runs; i++ {
|
||||||
|
arr := make([]int, n)
|
||||||
|
for i := range arr {
|
||||||
|
arr[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Shuffle(len(arr), func(i, j int) {
|
||||||
|
arr[i], arr[j] = arr[j], arr[i]
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Shuffle returned an error")
|
||||||
|
|
||||||
|
// Convert the permutation to a string key and count it.
|
||||||
|
key := fmt.Sprintf("%v", arr)
|
||||||
|
permCounts[key]++
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, fact, len(permCounts),
|
||||||
|
"Expected %d different permutations", fact,
|
||||||
|
)
|
||||||
|
|
||||||
|
wantCount := float64(runs) / float64(fact)
|
||||||
|
margin := 0.15
|
||||||
|
minAcceptable := int(wantCount * (1 - margin))
|
||||||
|
maxAcceptable := int(wantCount * (1 + margin))
|
||||||
|
|
||||||
|
for perm, count := range permCounts {
|
||||||
|
assert.True(t,
|
||||||
|
count >= minAcceptable && count <= maxAcceptable,
|
||||||
|
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||||
|
perm, count, wantCount, wantCount*margin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("distribution", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// Track which positions received which random indices
|
||||||
|
n := 100
|
||||||
|
posCounts := make([]map[int]int, n)
|
||||||
|
for i := range posCounts {
|
||||||
|
posCounts[i] = make(map[int]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
runs := 3000
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
err := Shuffle(n, func(i, j int) {
|
||||||
|
posCounts[i][j]++
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "Shuffle returned an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each position, check that it received a reasonable distribution.
|
||||||
|
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||||
|
// Calculate how many unique positions we should expect.
|
||||||
|
// Position i should receive random positions from 0 to i, and
|
||||||
|
// allow for some statistical variation.
|
||||||
|
want := int(float64(i+1) * 0.9)
|
||||||
|
assert.GreaterOrEqual(t,
|
||||||
|
len(posCounts[i]), want,
|
||||||
|
"Position %d: expected ~%d unique indices, got %d",
|
||||||
|
i, want, len(posCounts[i]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShuffle(b *testing.B) {
|
||||||
|
ranges := []int{32, 64, 128, 1024, 4096}
|
||||||
|
for _, n := range ranges {
|
||||||
|
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = Shuffle(n, func(_, _ int) {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShuffleSlice(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("empty slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{}
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Empty(t, slice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single element", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{42}
|
||||||
|
origSlice := make([]int, len(slice))
|
||||||
|
copy(origSlice, slice)
|
||||||
|
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t,
|
||||||
|
origSlice, slice, "Single element slice should remain unchanged",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("two elements", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := []int{1, 2}
|
||||||
|
origSlice := make([]int, len(slice))
|
||||||
|
copy(origSlice, slice)
|
||||||
|
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// With two elements, the slice might remain the same or be swapped
|
||||||
|
assert.Len(t, slice, len(origSlice))
|
||||||
|
assert.ElementsMatch(t, origSlice, slice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("basic", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
slice := make([]int, 100)
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
sliceCopy := make([]int, len(slice))
|
||||||
|
copy(sliceCopy, slice)
|
||||||
|
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||||
|
|
||||||
|
assert.NotEqual(t,
|
||||||
|
sliceCopy, slice, "ShuffleSlice did not change the slice",
|
||||||
|
)
|
||||||
|
assert.ElementsMatch(t,
|
||||||
|
sliceCopy, slice, "ShuffleSlice changed elements",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
strSlice := []string{"a", "b", "c", "d", "e"}
|
||||||
|
strCopy := make([]string, len(strSlice))
|
||||||
|
copy(strCopy, strSlice)
|
||||||
|
|
||||||
|
err := ShuffleSlice(strSlice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, strCopy, strSlice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("struct slice", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type testStruct struct {
|
||||||
|
id int
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
structSlice := []testStruct{
|
||||||
|
{1, "one"},
|
||||||
|
{2, "two"},
|
||||||
|
{3, "three"},
|
||||||
|
{4, "four"},
|
||||||
|
}
|
||||||
|
structCopy := make([]testStruct, len(structSlice))
|
||||||
|
copy(structCopy, structSlice)
|
||||||
|
|
||||||
|
err := ShuffleSlice(structSlice)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.ElementsMatch(t, structCopy, structSlice)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("all permutations", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Use a small slice of 5 elements to make it feasible to track all
|
||||||
|
// permutations.
|
||||||
|
n := 5
|
||||||
|
fact := factorial(n) // 120
|
||||||
|
runs := fact * 3000 // 360000
|
||||||
|
|
||||||
|
permCounts := make(map[string]int)
|
||||||
|
for i := 0; i < runs; i++ {
|
||||||
|
slice := make([]int, n)
|
||||||
|
for j := range slice {
|
||||||
|
slice[j] = j
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||||
|
|
||||||
|
// Convert the permutation to a string key and count it.
|
||||||
|
key := fmt.Sprintf("%v", slice)
|
||||||
|
permCounts[key]++
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, fact, len(permCounts),
|
||||||
|
"Expected %d different permutations", fact,
|
||||||
|
)
|
||||||
|
|
||||||
|
wantCount := float64(runs) / float64(fact)
|
||||||
|
margin := 0.15
|
||||||
|
minAcceptable := int(wantCount * (1 - margin))
|
||||||
|
maxAcceptable := int(wantCount * (1 + margin))
|
||||||
|
|
||||||
|
for perm, count := range permCounts {
|
||||||
|
assert.True(t,
|
||||||
|
count >= minAcceptable && count <= maxAcceptable,
|
||||||
|
"Non-uniform distribution for %s: count=%d, expected=%v±%v",
|
||||||
|
perm, count, wantCount, wantCount*margin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("distribution", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Track where each original index ends up after shuffling
|
||||||
|
n := 100
|
||||||
|
// posCounts[originalPos][newPos] tracks how many times
|
||||||
|
// the element originally at position i ended up at position j
|
||||||
|
posCounts := make([]map[int]int, n)
|
||||||
|
for i := range posCounts {
|
||||||
|
posCounts[i] = make(map[int]int)
|
||||||
|
}
|
||||||
|
|
||||||
|
runs := 3000
|
||||||
|
for run := 0; run < runs; run++ {
|
||||||
|
// Create a slice where the value is its original position
|
||||||
|
slice := make([]int, n)
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ShuffleSlice(slice)
|
||||||
|
require.NoError(t, err, "ShuffleSlice returned an error")
|
||||||
|
|
||||||
|
// Track where each original position ended up
|
||||||
|
for newPos, origPos := range slice {
|
||||||
|
posCounts[origPos][newPos]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each original position, check that it was distributed
|
||||||
|
// reasonably across all possible new positions
|
||||||
|
for i := n - 1; i >= n-len(posCounts); i-- {
|
||||||
|
// Calculate how many unique positions we should expect.
|
||||||
|
// Position i should receive random positions from 0 to i, and
|
||||||
|
// allow for some statistical variation.
|
||||||
|
want := int(float64(i+1) * 0.9)
|
||||||
|
assert.GreaterOrEqual(t,
|
||||||
|
len(posCounts[i]), want,
|
||||||
|
"Original position %d: expected ~%d unique positions, got %d",
|
||||||
|
i, want, len(posCounts[i]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkShuffleSlice(b *testing.B) {
|
||||||
|
ranges := []int{32, 64, 128, 1024, 4096}
|
||||||
|
for _, n := range ranges {
|
||||||
|
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
|
||||||
|
b.StopTimer()
|
||||||
|
slice := make([]int, n)
|
||||||
|
for i := range slice {
|
||||||
|
slice[i] = i
|
||||||
|
}
|
||||||
|
b.StartTimer()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_ = ShuffleSlice(slice)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user