diff --git a/README.md b/README.md index a2d202f1..73291167 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Supported helpers for slices: - Map - Reduce - ForEach +- Times - Uniq - UniqBy - GroupBy @@ -59,6 +60,7 @@ Supported helpers for slices: - Shuffle - Reverse - Fill +- Repeat - ToMap Supported helpers for maps: @@ -99,6 +101,7 @@ Other functional programming helpers: - Switch / Case / Default - ToPtr - ToSlicePtr +- Attempt Constraints: @@ -183,6 +186,30 @@ lop.ForEach[string]([]string{"hello", "world"}, func(x string, _ int) { // prints "hello\nworld\n" or "world\nhello\n" ``` +### Times + +Times invokes the iteratee n times, returning an array of the results of each invocation. The iteratee is invoked with index as argument. + +```go +import "github.com/samber/lo" + +lo.Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) +}) +// []string{"0", "1", "2"} +``` + +Parallel processing: like `lo.Times()`, but callback is called in goroutine. + +```go +import lop "github.com/samber/lo/parallel" + +lop.Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) +}) +// []string{"0", "1", "2"} +``` + ### Uniq Returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. The order of result values is determined by the order they occur in the array. @@ -208,7 +235,9 @@ uniqValues := lo.UniqBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { Returns an object composed of keys generated from the results of running each element of collection through iteratee. ```go -groups := GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { +import lo "github.com/samber/lo" + +groups := lo.GroupBy[int, int]([]int{0, 1, 2, 3, 4, 5}, func(i int) int { return i%3 }) // map[int][]int{0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}} @@ -248,6 +277,8 @@ lo.Chunk[int]([]int{0}, 2) Returns an array of elements split into groups. The order of grouped values is determined by the order they occur in collection. The grouping is generated from the results of running each element of collection through iteratee. ```go +import lo "github.com/samber/lo" + partitions := lo.PartitionBy[int, string]([]int{-2, -1, 0, 1, 2, 3, 4, 5}, func(x int) string { if x < 0 { return "negative" @@ -319,6 +350,23 @@ initializedSlice := lo.Fill[foo]([]foo{foo{"a"}, foo{"a"}}, foo{"b"}) // []foo{foo{"b"}, foo{"b"}} ``` +### Repeat + +Builds a slice with N copies of initial value. + +```go +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} + +initializedSlice := lo.Repeat[foo](2, foo{"a"}) +// []foo{foo{"a"}, foo{"a"}} +``` + ### ToMap Transforms a slice or an array of structs to a map based on a pivot callback. @@ -643,6 +691,42 @@ ptr := lo.ToSlicePtr[string]([]string{"hello", "world"}) // []*string{"hello", "world"} ``` +### Attempt + +Invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. + +```go +iter, err := lo.Attempt(42, func(i int) error { + if i == 5 { + return nil + } + + return fmt.Errorf("failed") +}) +// 6 +// nil + +iter, err := lo.Attempt(2, func(i int) error { + if i == 5 { + return nil + } + + return fmt.Errorf("failed") +}) +// 2 +// error "failed" + +iter, err := lo.Attempt(0, func(i int) error { + if i < 42 { + return fmt.Errorf("failed") + } + + return nil +}) +// 43 +// nil +``` + ## 🛩 Benchmark We executed a simple benchmark with the a dead-simple `lo.Map` loop: diff --git a/parallel/slice.go b/parallel/slice.go index 1a6eee2a..870da856 100644 --- a/parallel/slice.go +++ b/parallel/slice.go @@ -44,6 +44,33 @@ func ForEach[T any](collection []T, iteratee func(T, int)) { wg.Wait() } +// Times invokes the iteratee n times, returning an array of the results of each invocation. +// The iteratee is invoked with index as argument. +// `iteratee` is call in parallel. +func Times[T any](count int, iteratee func(int) T) []T { + result := make([]T, count) + + var mu sync.Mutex + var wg sync.WaitGroup + wg.Add(count) + + for i := 0; i < count; i++ { + go func(_i int) { + item := iteratee(_i) + + mu.Lock() + result[_i] = item + mu.Unlock() + + wg.Done() + }(i) + } + + wg.Wait() + + return result +} + // GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. // `iteratee` is call in parallel. func GroupBy[T any, U comparable](collection []T, iteratee func(T) U) map[U][]T { diff --git a/parallel/slice_test.go b/parallel/slice_test.go index 4539f7a8..1426c009 100644 --- a/parallel/slice_test.go +++ b/parallel/slice_test.go @@ -23,6 +23,17 @@ func TestMap(t *testing.T) { is.Equal(result2, []string{"1", "2", "3", "4"}) } +func TestTimes(t *testing.T) { + is := assert.New(t) + + result1 := Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) + }) + + is.Equal(len(result1), 3) + is.Equal(result1, []string{"0", "1", "2"}) +} + func TestGroupBy(t *testing.T) { is := assert.New(t) @@ -30,8 +41,8 @@ func TestGroupBy(t *testing.T) { return i % 3 }) - is.Equal(len(result1), 3) - is.Equal(result1, map[int][]int{ + is.EqualValues(len(result1), 3) + is.EqualValues(result1, map[int][]int{ 0: []int{0, 3}, 1: []int{1, 4}, 2: []int{2, 5}, diff --git a/retry.go b/retry.go new file mode 100644 index 00000000..5b4dc51b --- /dev/null +++ b/retry.go @@ -0,0 +1,16 @@ +package lo + +// Attempt invokes a function N times until it returns valid output. Returning either the caught error or nil. When first argument is less than `1`, the function runs until a sucessfull response is returned. +func Attempt(maxIteration int, f func(int) error) (int, error) { + var err error + + for i := 0; maxIteration <= 0 || i < maxIteration; i++ { + // for retries >= 0 { + err = f(i) + if err == nil { + return i + 1, nil + } + } + + return maxIteration, err +} diff --git a/retry_test.go b/retry_test.go new file mode 100644 index 00000000..45f46fd6 --- /dev/null +++ b/retry_test.go @@ -0,0 +1,48 @@ +package lo + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAttempt(t *testing.T) { + is := assert.New(t) + + err := fmt.Errorf("failed") + + iter1, err1 := Attempt(42, func(i int) error { + return nil + }) + iter2, err2 := Attempt(42, func(i int) error { + if i == 5 { + return nil + } + + return err + }) + iter3, err3 := Attempt(2, func(i int) error { + if i == 5 { + return nil + } + + return err + }) + iter4, err4 := Attempt(0, func(i int) error { + if i < 42 { + return fmt.Errorf("failed") + } + + return nil + }) + + is.Equal(iter1, 1) + is.Equal(err1, nil) + is.Equal(iter2, 6) + is.Equal(err2, nil) + is.Equal(iter3, 2) + is.Equal(err3, err) + is.Equal(iter4, 43) + is.Equal(err4, nil) +} diff --git a/slice.go b/slice.go index 28562f17..a0955177 100644 --- a/slice.go +++ b/slice.go @@ -43,6 +43,18 @@ func ForEach[T any](collection []T, iteratee func(T, int)) { } } +// Times invokes the iteratee n times, returning an array of the results of each invocation. +// The iteratee is invoked with index as argument. +func Times[T any](count int, iteratee func(int) T) []T { + result := make([]T, count) + + for i := 0; i < count; i++ { + result[i] = iteratee(i) + } + + return result +} + // Uniq returns a duplicate-free version of an array, in which only the first occurrence of each element is kept. // The order of result values is determined by the order they occur in the array. func Uniq[T comparable](collection []T) []T { @@ -193,6 +205,17 @@ func Fill[T Clonable[T]](collection []T, initial T) []T { return result } +// Repeat builds a slice with N copies of initial value. +func Repeat[T Clonable[T]](count int, initial T) []T { + result := make([]T, 0, count) + + for i := 0; i < count; i++ { + result = append(result, initial.Clone()) + } + + return result +} + // ToMap transforms a slice or an array of structs to a map based on a pivot callback. func ToMap[K comparable, V any](collection []V, iteratee func(V) K) map[K]V { result := make(map[K]V, len(collection)) diff --git a/slice_test.go b/slice_test.go index dc2a962b..7cb0f2b4 100644 --- a/slice_test.go +++ b/slice_test.go @@ -7,6 +7,14 @@ import ( "github.com/stretchr/testify/assert" ) +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} + func TestFilter(t *testing.T) { is := assert.New(t) @@ -39,6 +47,17 @@ func TestMap(t *testing.T) { is.Equal(result2, []string{"1", "2", "3", "4"}) } +func TestTimes(t *testing.T) { + is := assert.New(t) + + result1 := Times[string](3, func(i int) string { + return strconv.FormatInt(int64(i), 10) + }) + + is.Equal(len(result1), 3) + is.Equal(result1, []string{"0", "1", "2"}) +} + func TestReduce(t *testing.T) { is := assert.New(t) @@ -151,14 +170,6 @@ func TestReverse(t *testing.T) { is.Equal(result3, []int{}) } -type foo struct { - bar string -} - -func (f foo) Clone() foo { - return foo{f.bar} -} - func TestFill(t *testing.T) { is := assert.New(t) @@ -169,6 +180,16 @@ func TestFill(t *testing.T) { is.Equal(result2, []foo{}) } +func TestRepeat(t *testing.T) { + is := assert.New(t) + + result1 := Repeat[foo](2, foo{"a"}) + result2 := Repeat[foo](0, foo{"a"}) + + is.Equal(result1, []foo{foo{"a"}, foo{"a"}}) + is.Equal(result2, []foo{}) +} + func TestToMap(t *testing.T) { is := assert.New(t)