mirror of
https://github.com/aykhans/go-utils.git
synced 2025-10-15 18:25:57 +00:00
first commit
This commit is contained in:
92
slice/slice.go
Normal file
92
slice/slice.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package slice
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cycle returns a function that cycles through the provided items infinitely.
|
||||
// Each call to the returned function returns the next item in sequence, wrapping
|
||||
// back to the first item after the last one is returned.
|
||||
//
|
||||
// If no items are provided, the returned function will always return the zero
|
||||
// value for type T.
|
||||
//
|
||||
// The returned function is not safe for concurrent use. If you need to call it
|
||||
// from multiple goroutines, you must synchronize access with a mutex or similar.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// next := Cycle(1, 2, 3)
|
||||
// fmt.Println(next()) // 1
|
||||
// fmt.Println(next()) // 2
|
||||
// fmt.Println(next()) // 3
|
||||
// fmt.Println(next()) // 1
|
||||
func Cycle[T any](items ...T) func() T {
|
||||
if len(items) == 0 {
|
||||
var zero T
|
||||
return func() T { return zero }
|
||||
}
|
||||
|
||||
index := 0
|
||||
return func() T {
|
||||
item := items[index]
|
||||
index = (index + 1) % len(items)
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
// RandomCycle returns a function that cycles through the provided items with
|
||||
// randomization. It cycles through all items sequentially, but when it completes
|
||||
// a full cycle, it randomly picks a new starting point for the next cycle.
|
||||
//
|
||||
// The localRand parameter can be used to provide a custom random number generator.
|
||||
// If nil, a new generator will be created using the current time as the seed.
|
||||
//
|
||||
// The returned function is not safe for concurrent use. If you need to call it
|
||||
// from multiple goroutines, you must synchronize access with a mutex or similar.
|
||||
//
|
||||
// Special cases:
|
||||
// - If no items are provided, the returned function always returns the zero value for type T.
|
||||
// - If only one item is provided, the returned function always returns that item.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// next := RandomCycle(nil, "a", "b", "c")
|
||||
// // Might produce: "b", "c", "a", "c", "a", "b", ...
|
||||
// // (cycles through all items, then starts from a random position)
|
||||
func RandomCycle[T any](localRand *rand.Rand, items ...T) func() T {
|
||||
switch sliceLen := len(items); sliceLen {
|
||||
case 0:
|
||||
var zero T
|
||||
return func() T { return zero }
|
||||
case 1:
|
||||
return func() T { return items[0] }
|
||||
default:
|
||||
if localRand == nil {
|
||||
//nolint:gosec
|
||||
localRand = rand.New(
|
||||
rand.NewPCG(
|
||||
uint64(time.Now().UnixNano()),
|
||||
uint64(time.Now().UnixNano()>>32),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
currentIndex := localRand.IntN(sliceLen)
|
||||
stopIndex := currentIndex
|
||||
return func() T {
|
||||
item := items[currentIndex]
|
||||
currentIndex++
|
||||
if currentIndex == sliceLen {
|
||||
currentIndex = 0
|
||||
}
|
||||
if currentIndex == stopIndex {
|
||||
currentIndex = localRand.IntN(sliceLen)
|
||||
stopIndex = currentIndex
|
||||
}
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
}
|
324
slice/slice_test.go
Normal file
324
slice/slice_test.go
Normal file
@@ -0,0 +1,324 @@
|
||||
package slice
|
||||
|
||||
import (
|
||||
"math/rand/v2"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCycle(t *testing.T) {
|
||||
t.Run("cycles through items", func(t *testing.T) {
|
||||
next := Cycle(1, 2, 3)
|
||||
|
||||
assert.Equal(t, 1, next())
|
||||
assert.Equal(t, 2, next())
|
||||
assert.Equal(t, 3, next())
|
||||
assert.Equal(t, 1, next()) // wraps back to first
|
||||
assert.Equal(t, 2, next())
|
||||
assert.Equal(t, 3, next())
|
||||
assert.Equal(t, 1, next()) // wraps again
|
||||
})
|
||||
|
||||
t.Run("cycles through single item", func(t *testing.T) {
|
||||
next := Cycle(42)
|
||||
|
||||
assert.Equal(t, 42, next())
|
||||
assert.Equal(t, 42, next())
|
||||
assert.Equal(t, 42, next())
|
||||
})
|
||||
|
||||
t.Run("returns zero value for empty slice", func(t *testing.T) {
|
||||
next := Cycle[int]()
|
||||
|
||||
assert.Equal(t, 0, next())
|
||||
assert.Equal(t, 0, next())
|
||||
assert.Equal(t, 0, next())
|
||||
})
|
||||
|
||||
t.Run("works with string type", func(t *testing.T) {
|
||||
next := Cycle("a", "b", "c")
|
||||
|
||||
assert.Equal(t, "a", next())
|
||||
assert.Equal(t, "b", next())
|
||||
assert.Equal(t, "c", next())
|
||||
assert.Equal(t, "a", next())
|
||||
})
|
||||
|
||||
t.Run("works with struct type", func(t *testing.T) {
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
next := Cycle(
|
||||
Point{1, 2},
|
||||
Point{3, 4},
|
||||
)
|
||||
|
||||
assert.Equal(t, Point{1, 2}, next())
|
||||
assert.Equal(t, Point{3, 4}, next())
|
||||
assert.Equal(t, Point{1, 2}, next())
|
||||
})
|
||||
|
||||
t.Run("works with pointer type", func(t *testing.T) {
|
||||
val1, val2 := 10, 20
|
||||
next := Cycle(&val1, &val2)
|
||||
|
||||
assert.Equal(t, &val1, next())
|
||||
assert.Equal(t, &val2, next())
|
||||
assert.Equal(t, &val1, next())
|
||||
})
|
||||
|
||||
t.Run("empty string slice returns empty string", func(t *testing.T) {
|
||||
next := Cycle[string]()
|
||||
|
||||
assert.Empty(t, next())
|
||||
assert.Empty(t, next())
|
||||
})
|
||||
|
||||
t.Run("multiple cycles work correctly", func(t *testing.T) {
|
||||
next := Cycle("x", "y")
|
||||
|
||||
// First cycle
|
||||
assert.Equal(t, "x", next())
|
||||
assert.Equal(t, "y", next())
|
||||
// Second cycle
|
||||
assert.Equal(t, "x", next())
|
||||
assert.Equal(t, "y", next())
|
||||
// Third cycle
|
||||
assert.Equal(t, "x", next())
|
||||
assert.Equal(t, "y", next())
|
||||
})
|
||||
|
||||
t.Run("each function instance maintains its own state", func(t *testing.T) {
|
||||
next1 := Cycle(1, 2, 3)
|
||||
next2 := Cycle(1, 2, 3)
|
||||
|
||||
assert.Equal(t, 1, next1())
|
||||
assert.Equal(t, 1, next2())
|
||||
assert.Equal(t, 2, next1())
|
||||
assert.Equal(t, 2, next2())
|
||||
assert.Equal(t, 3, next1())
|
||||
assert.Equal(t, 1, next1())
|
||||
assert.Equal(t, 3, next2())
|
||||
})
|
||||
|
||||
t.Run("works with boolean type", func(t *testing.T) {
|
||||
next := Cycle(true, false)
|
||||
|
||||
assert.True(t, next())
|
||||
assert.False(t, next())
|
||||
assert.True(t, next())
|
||||
assert.False(t, next())
|
||||
})
|
||||
|
||||
t.Run("works with float type", func(t *testing.T) {
|
||||
next := Cycle(1.1, 2.2, 3.3)
|
||||
|
||||
assert.InDelta(t, 1.1, next(), 0.001)
|
||||
assert.InDelta(t, 2.2, next(), 0.001)
|
||||
assert.InDelta(t, 3.3, next(), 0.001)
|
||||
assert.InDelta(t, 1.1, next(), 0.001)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRandomCycle(t *testing.T) {
|
||||
t.Run("returns zero value for empty slice", func(t *testing.T) {
|
||||
next := RandomCycle[int](nil)
|
||||
|
||||
assert.Equal(t, 0, next())
|
||||
assert.Equal(t, 0, next())
|
||||
assert.Equal(t, 0, next())
|
||||
})
|
||||
|
||||
t.Run("returns same item for single item slice", func(t *testing.T) {
|
||||
next := RandomCycle(nil, 42)
|
||||
|
||||
assert.Equal(t, 42, next())
|
||||
assert.Equal(t, 42, next())
|
||||
assert.Equal(t, 42, next())
|
||||
})
|
||||
|
||||
t.Run("cycles through all items with seeded random", func(t *testing.T) {
|
||||
seed := rand.NewPCG(1, 2)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, "a", "b", "c")
|
||||
|
||||
// Collect items to verify all are returned
|
||||
seen := make(map[string]bool)
|
||||
for range 100 {
|
||||
item := next()
|
||||
seen[item] = true
|
||||
}
|
||||
|
||||
// All items should have been seen
|
||||
assert.True(t, seen["a"], "should see 'a'")
|
||||
assert.True(t, seen["b"], "should see 'b'")
|
||||
assert.True(t, seen["c"], "should see 'c'")
|
||||
})
|
||||
|
||||
t.Run("works with seeded random generator", func(t *testing.T) {
|
||||
// Using same seed should produce same sequence
|
||||
seed1 := rand.NewPCG(42, 42)
|
||||
r1 := rand.New(seed1)
|
||||
next1 := RandomCycle(r1, 1, 2, 3)
|
||||
|
||||
seed2 := rand.NewPCG(42, 42)
|
||||
r2 := rand.New(seed2)
|
||||
next2 := RandomCycle(r2, 1, 2, 3)
|
||||
|
||||
// First few calls should match
|
||||
for range 10 {
|
||||
assert.Equal(t, next1(), next2(), "calls with same seed should match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("creates own random generator when nil provided", func(t *testing.T) {
|
||||
next := RandomCycle[int](nil, 1, 2, 3)
|
||||
|
||||
// Should not panic and should return valid values
|
||||
for range 10 {
|
||||
val := next()
|
||||
assert.Contains(t, []int{1, 2, 3}, val)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("eventually returns all items in cycle", func(t *testing.T) {
|
||||
seed := rand.NewPCG(123, 456)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, 1, 2, 3, 4, 5)
|
||||
|
||||
// Track items seen in current "window"
|
||||
for range 5 {
|
||||
seen := make(map[int]int)
|
||||
// Collect enough items to ensure we see at least one full cycle
|
||||
for range 10 {
|
||||
item := next()
|
||||
seen[item]++
|
||||
assert.Contains(t, []int{1, 2, 3, 4, 5}, item)
|
||||
}
|
||||
// Should see multiple items
|
||||
assert.GreaterOrEqual(t, len(seen), 3, "should see at least 3 different items")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("works with string type", func(t *testing.T) {
|
||||
seed := rand.NewPCG(1, 2)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, "x", "y", "z")
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for range 50 {
|
||||
item := next()
|
||||
seen[item] = true
|
||||
assert.Contains(t, []string{"x", "y", "z"}, item)
|
||||
}
|
||||
|
||||
assert.True(t, seen["x"])
|
||||
assert.True(t, seen["y"])
|
||||
assert.True(t, seen["z"])
|
||||
})
|
||||
|
||||
t.Run("works with struct type", func(t *testing.T) {
|
||||
type Item struct {
|
||||
ID int
|
||||
}
|
||||
seed := rand.NewPCG(1, 2)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, Item{1}, Item{2}, Item{3})
|
||||
|
||||
seen := make(map[int]bool)
|
||||
for range 50 {
|
||||
item := next()
|
||||
seen[item.ID] = true
|
||||
}
|
||||
|
||||
assert.True(t, seen[1])
|
||||
assert.True(t, seen[2])
|
||||
assert.True(t, seen[3])
|
||||
})
|
||||
|
||||
t.Run("each function instance maintains its own state", func(t *testing.T) {
|
||||
seed1 := rand.NewPCG(1, 2)
|
||||
r1 := rand.New(seed1)
|
||||
next1 := RandomCycle(r1, 1, 2, 3)
|
||||
|
||||
seed2 := rand.NewPCG(3, 5)
|
||||
r2 := rand.New(seed2)
|
||||
next2 := RandomCycle(r2, 1, 2, 3)
|
||||
|
||||
// Get a few values from each
|
||||
vals1 := []int{next1(), next1(), next1()}
|
||||
vals2 := []int{next2(), next2(), next2()}
|
||||
|
||||
// They should be different (very high probability with different seeds)
|
||||
assert.NotEqual(t, vals1, vals2, "different seeds should produce different sequences")
|
||||
})
|
||||
|
||||
t.Run("with two items", func(t *testing.T) {
|
||||
seed := rand.NewPCG(99, 100)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, "a", "b")
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for range 20 {
|
||||
item := next()
|
||||
seen[item] = true
|
||||
assert.Contains(t, []string{"a", "b"}, item)
|
||||
}
|
||||
|
||||
assert.True(t, seen["a"])
|
||||
assert.True(t, seen["b"])
|
||||
})
|
||||
|
||||
t.Run("deterministic with same seed across multiple cycles", func(t *testing.T) {
|
||||
// First run
|
||||
seed1 := rand.NewPCG(777, 888)
|
||||
r1 := rand.New(seed1)
|
||||
next1 := RandomCycle(r1, 10, 20, 30)
|
||||
sequence1 := make([]int, 20)
|
||||
for i := range 20 {
|
||||
sequence1[i] = next1()
|
||||
}
|
||||
|
||||
// Second run with same seed
|
||||
seed2 := rand.NewPCG(777, 888)
|
||||
r2 := rand.New(seed2)
|
||||
next2 := RandomCycle(r2, 10, 20, 30)
|
||||
sequence2 := make([]int, 20)
|
||||
for i := range 20 {
|
||||
sequence2[i] = next2()
|
||||
}
|
||||
|
||||
assert.Equal(t, sequence1, sequence2, "same seed should produce same sequence")
|
||||
})
|
||||
|
||||
t.Run("empty string slice returns empty string", func(t *testing.T) {
|
||||
next := RandomCycle[string](nil)
|
||||
|
||||
assert.Empty(t, next())
|
||||
assert.Empty(t, next())
|
||||
})
|
||||
|
||||
t.Run("large number of items", func(t *testing.T) {
|
||||
items := make([]int, 100)
|
||||
for i := range 100 {
|
||||
items[i] = i
|
||||
}
|
||||
|
||||
seed := rand.NewPCG(42, 43)
|
||||
r := rand.New(seed)
|
||||
next := RandomCycle(r, items...)
|
||||
|
||||
seen := make(map[int]bool)
|
||||
// Call enough times to likely see all items
|
||||
for range 1000 {
|
||||
item := next()
|
||||
seen[item] = true
|
||||
assert.GreaterOrEqual(t, item, 0)
|
||||
assert.LessOrEqual(t, item, 99)
|
||||
}
|
||||
|
||||
// Should see a good variety of items
|
||||
assert.Greater(t, len(seen), 90, "should see most items with 1000 calls")
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user