first commit

This commit is contained in:
2025-10-11 19:53:28 +04:00
commit 0d53670ab7
18 changed files with 2413 additions and 0 deletions

92
slice/slice.go Normal file
View 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
View 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")
})
}