Merge branch 'master' into master

This commit is contained in:
Samuel Berthe
2022-03-20 01:56:08 +01:00
committed by GitHub
12 changed files with 601 additions and 46 deletions
+64
View File
@@ -1,5 +1,69 @@
# Changelog
## 1.10.0 (2022-03-11)
Adding:
- Try
- Try{0-6}
- TryWitchValue
- TryCatch
- TryCatchWitchValue
- Debounce
## 1.10.0 (2022-03-11)
Adding:
- Range
- RangeFrom
- RangeWithSteps
## 1.9.0 (2022-03-10)
Added
- Drop
- DropRight
- DropWhile
- DropRightWhile
## 1.8.0 (2022-03-10)
Adding Union.
## 1.7.0 (2022-03-09)
Adding ContainBy
Adding MapValues
Adding FlatMap
## 1.6.0 (2022-03-07)
Fixed PartitionBy.
Adding Sample
Adding Samples
## 1.5.0 (2022-03-07)
Adding Times
Adding Attempt
Adding Repeat
## 1.4.0 (2022-03-07)
- adding tuple types (2->9)
- adding Zip + Unzip
- adding lo.PartitionBy + lop.PartitionBy
- adding lop.GroupBy
- fixing Nth
## 1.3.0 (2022-03-03)
Last and Nth return errors
+1 -1
View File
@@ -3,6 +3,6 @@ FROM golang:1.18rc1-bullseye
WORKDIR /go/src/github.com/samber/lo
COPY Makefile go.* /go/src/github.com/samber/lo/
COPY Makefile go.* ./
RUN make tools
+2 -2
View File
@@ -12,12 +12,12 @@ build:
test:
go test -race -v ./...
watch-test:
reflex -R assets.go -t 50ms -s -- sh -c 'gotest -race -v ./...'
reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
bench:
go test -benchmem -count 3 -bench ./...
watch-bench:
reflex -R assets.go -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
coverage:
${BIN} test -v -coverprofile cover.out .
+149 -29
View File
@@ -64,12 +64,13 @@ Supported helpers for slices:
- Reverse
- Fill
- Repeat
- ToMap
- KeyBy
- Drop
- DropRight
- DropWhile
- DropRightWhile
- Reject
- Range / RangeFrom / RangeWithSteps
Supported helpers for maps:
@@ -114,8 +115,18 @@ Other functional programming helpers:
- Switch / Case / Default
- ToPtr
- ToSlicePtr
Time based helpers:
- Attempt
- Range / RangeFrom / RangeWithSteps
- Debounce
Error handling:
- Try
- TryCatch
- TryWithErrorValue
- TryCatchWithErrorValue
Constraints:
@@ -406,15 +417,28 @@ initializedSlice := lo.Repeat[foo](2, foo{"a"})
// []foo{foo{"a"}, foo{"a"}}
```
### ToMap
### KeyBy
Transforms a slice or an array of structs to a map based on a pivot callback.
```go
m := lo.ToMap[int, string]([]string{"a", "aa", "aaa"}, func(str string) int {
m := lo.KeyBy[int, string]([]string{"a", "aa", "aaa"}, func(str string) int {
return len(str)
})
// map[int]string{1: "a", 2: "aa", 3: "aaa"}
type Character struct {
dir string
code int
}
characters := []Character{
{dir: "left", code: 97},
{dir: "right", code: 100},
}
result := lo.KeyBy[string, Character](characters, func(char Character) string {
return string(rune(char.code))
})
//map[a:{dir:left code:97} d:{dir:right code:100}]
```
### Drop
@@ -443,7 +467,7 @@ Drop elements from the beginning of a slice or array while the predicate returns
l := lo.DropWhile[string]([]string{"a", "aa", "aaa", "aa", "aa"}, func(val string) bool {
return len(val) <= 2
})
// []string{"aaa", "aa", "a"}
// []string{"aaa", "aa", "aa"}
```
### DropRightWhile
@@ -468,6 +492,36 @@ odd := lo.Reject[int]([]int{1, 2, 3, 4}, func(x int, _ int) bool {
// []int{1, 3}
```
### Range / RangeFrom / RangeWithSteps
Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end.
```go
result := Range(4)
// [0, 1, 2, 3]
result := Range(-4);
// [0, -1, -2, -3]
result := RangeFrom(1, 5);
// [1, 2, 3, 4]
result := RangeFrom[float64](1.0, 5);
// [1.0, 2.0, 3.0, 4.0]
result := RangeWithSteps(0, 20, 5);
// [0, 5, 10, 15]
result := RangeWithSteps[float32](-1.0, -4.0, -1.0);
// [-1.0, -2.0, -3.0]
result := RangeWithSteps(1, 4, -1);
// []
result := Range(0);
// []
```
### Keys
Creates an array of the map keys.
@@ -860,37 +914,103 @@ iter, err := lo.Attempt(0, func(i int) error {
// nil
```
### Range / RangeFrom / RangeWithSteps
### Debounce
Creates an array of numbers (positive and/or negative) progressing from start up to, but not including end.
`NewDebounce` creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed, until `cancel` is called.
```go
result := Range(4)
// [0, 1, 2, 3]
f := func() {
println("Called once after 100ms when debounce stopped invoking!")
}
result := Range(-4);
// [0, -1, -2, -3]
debounce, cancel := lo.NewDebounce(100 * time.Millisecond, f)
for j := 0; j < 10; j++ {
debounce()
}
result := RangeFrom(1, 5);
// [1, 2, 3, 4]
result := RangeFrom[float64](1.0, 5);
// [1.0, 2.0, 3.0, 4.0]
result := RangeWithSteps(0, 20, 5);
// [0, 5, 10, 15]
result := RangeWithSteps[float32](-1.0, -4.0, -1.0);
// [-1.0, -2.0, -3.0]
result := RangeWithSteps(1, 4, -1);
// []
result := Range(0);
// []
time.Sleep(1 * time.Second)
cancel()
```
For more advanced retry strategies (delay, exponential backoff...), please take a look on [cenkalti/backoff](https://github.com/cenkalti/backoff).
## Try
Calls the function and return false in case of error and on panic.
```go
ok := lo.Try(func() error {
panic("error")
return nil
})
// false
ok := lo.Try(func() error {
return nil
})
// true
ok := lo.Try(func() error {
return fmt.Errorf("error")
})
// false
```
## Try{0->6}
The same behavior than `Try`, but callback returns 2 variables.
```go
ok := lo.Try2(func() (string, error) {
panic("error")
return "", nil
})
// false
```
## TryWithErrorValue
The same behavior than `Try`, but also returns value passed to panic.
```go
err, ok := lo.TryWithErrorValue(func() error {
panic("error")
return nil
})
// "error", false
```
## TryCatch
The same behavior than `Try`, but calls the catch function in case of error.
```go
caught := false
ok := lo.TryCatch(func() error {
panic("error")
return nil
}, func() {
caught = true
})
// false
// caught == true
```
## TryCatchWithErrorValue
The same behavior than `TryWithErrorValue`, but calls the catch function in case of error.
```go
caught := false
ok := lo.TryCatchWithErrorValue(func() error {
panic("error")
return nil
}, func(val any) {
caught = val == "error"
})
// false
// caught == true
```
## 🛩 Benchmark
+1 -1
View File
@@ -69,4 +69,4 @@ func MapValues[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) ma
}
return result
}
}
+60 -1
View File
@@ -1,5 +1,65 @@
package lo
import (
"sync"
"time"
)
type debounce struct {
after time.Duration
mu *sync.Mutex
timer *time.Timer
done bool
callbacks []func()
}
func (d *debounce) reset() *debounce {
d.mu.Lock()
defer d.mu.Unlock()
if d.done {
return d
}
if d.timer != nil {
d.timer.Stop()
}
d.timer = time.AfterFunc(d.after, func() {
for _, f := range d.callbacks {
f()
}
})
return d
}
func (d *debounce) cancel() {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil {
d.timer.Stop()
d.timer = nil
}
d.done = true
}
// NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed.
func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
d := &debounce{
after: duration,
mu: new(sync.Mutex),
timer: nil,
done: false,
callbacks: f,
}
return func() {
d.reset()
}, d.cancel
}
// 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
@@ -16,4 +76,3 @@ func Attempt(maxIteration int, f func(int) error) (int, error) {
}
// throttle ?
// debounce ?
+47
View File
@@ -3,6 +3,7 @@ package lo
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@@ -46,3 +47,49 @@ func TestAttempt(t *testing.T) {
is.Equal(iter4, 43)
is.Equal(err4, nil)
}
func TestDebounce(t *testing.T) {
f1 := func() {
println("1. Called once after 10ms when func stopped invoking!")
}
f2 := func() {
println("2. Called once after 10ms when func stopped invoking!")
}
f3 := func() {
println("3. Called once after 10ms when func stopped invoking!")
}
d1, _ := NewDebounce(10*time.Millisecond, f1)
// execute 3 times
for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
d1()
}
time.Sleep(20 * time.Millisecond)
}
d2, _ := NewDebounce(10*time.Millisecond, f2)
// execute once because it is always invoked and only last invoke is worked after 100ms
for i := 0; i < 3; i++ {
for j := 0; j < 5; j++ {
d2()
}
time.Sleep(5 * time.Millisecond)
}
time.Sleep(10 * time.Millisecond)
// execute once because it is canceled after 200ms.
d3, cancel := NewDebounce(10*time.Millisecond, f3)
for i := 0; i < 3; i++ {
for j := 0; j < 10; j++ {
d3()
}
time.Sleep(20 * time.Millisecond)
if i == 0 {
cancel()
}
}
}
+2 -2
View File
@@ -229,8 +229,8 @@ func Repeat[T Clonable[T]](count int, initial T) []T {
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 {
// KeyBy transforms a slice or an array of structs to a map based on a pivot callback.
func KeyBy[K comparable, V any](collection []V, iteratee func(V) K) map[K]V {
result := make(map[K]V, len(collection))
for _, v := range collection {
+2 -2
View File
@@ -210,10 +210,10 @@ func TestRepeat(t *testing.T) {
is.Equal(result2, []foo{})
}
func TestToMap(t *testing.T) {
func TestKeyBy(t *testing.T) {
is := assert.New(t)
result1 := ToMap[int, string]([]string{"a", "aa", "aaa"}, func(str string) int {
result1 := KeyBy[int, string]([]string{"a", "aa", "aaa"}, func(str string) int {
return len(str)
})
+106
View File
@@ -0,0 +1,106 @@
package lo
// Try calls the function and return false in case of error.
func Try(callback func() error) (ok bool) {
ok = true
defer func() {
if r := recover(); r != nil {
ok = false
}
}()
err := callback()
if err != nil {
ok = false
}
return
}
// Try0 has the same behavior than Try, but callback returns no variable.
func Try0[T any](callback func()) bool {
return Try(func() error {
callback()
return nil
})
}
// Try1 is an alias to Try
func Try1[T any](callback func() error) bool {
return Try(callback)
}
// Try2 has the same behavior than Try, but callback returns 2 variables.
func Try2[T any](callback func() (T, error)) bool {
return Try(func() error {
_, err := callback()
return err
})
}
// Try3 has the same behavior than Try, but callback returns 3 variables.
func Try3[T, R any](callback func() (T, R, error)) bool {
return Try(func() error {
_, _, err := callback()
return err
})
}
// Try4 has the same behavior than Try, but callback returns 4 variables.
func Try4[T, R, S any](callback func() (T, R, S, error)) bool {
return Try(func() error {
_, _, _, err := callback()
return err
})
}
// Try5 has the same behavior than Try, but callback returns 5 variables.
func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool {
return Try(func() error {
_, _, _, _, err := callback()
return err
})
}
// Try6 has the same behavior than Try, but callback returns 6 variables.
func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool {
return Try(func() error {
_, _, _, _, _, err := callback()
return err
})
}
// TryWithErrorValue has the same behavior than Try, but also returns value passed to panic.
func TryWithErrorValue(callback func() error) (errorValue any, ok bool) {
ok = true
defer func() {
if r := recover(); r != nil {
ok = false
errorValue = r
}
}()
err := callback()
if err != nil {
ok = false
errorValue = err
}
return
}
// TryCatch has the same behavior than Try, but calls the catch function in case of error.
func TryCatch(callback func() error, catch func()) {
if !Try(callback) {
catch()
}
}
// TryCatchWithErrorValue has the same behavior than TryWithErrorValue, but calls the catch function in case of error.
func TryCatchWithErrorValue(callback func() error, catch func(any)) {
if err, ok := TryWithErrorValue(callback); !ok {
catch(err)
}
}
+156
View File
@@ -0,0 +1,156 @@
package lo
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestTry(t *testing.T) {
is := assert.New(t)
is.False(Try(func() error {
panic("error")
return nil
}))
is.True(Try(func() error {
return nil
}))
is.False(Try(func() error {
return fmt.Errorf("fail")
}))
}
func TestTryFunctions(t *testing.T) {
is := assert.New(t)
is.True(Try2(func() (string, error) {
return "", nil
}))
is.True(Try3(func() (string, string, error) {
return "", "", nil
}))
is.True(Try4(func() (string, string, string, error) {
return "", "", "", nil
}))
is.True(Try5(func() (string, string, string, string, error) {
return "", "", "", "", nil
}))
is.True(Try6(func() (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}))
is.False(Try2(func() (string, error) {
panic("error")
return "", nil
}))
is.False(Try3(func() (string, string, error) {
panic("error")
return "", "", nil
}))
is.False(Try4(func() (string, string, string, error) {
panic("error")
return "", "", "", nil
}))
is.False(Try5(func() (string, string, string, string, error) {
panic("error")
return "", "", "", "", nil
}))
is.False(Try6(func() (string, string, string, string, string, error) {
panic("error")
return "", "", "", "", "", nil
}))
is.False(Try2(func() (string, error) {
return "", errors.New("foo")
}))
is.False(Try3(func() (string, string, error) {
return "", "", errors.New("foo")
}))
is.False(Try4(func() (string, string, string, error) {
return "", "", "", errors.New("foo")
}))
is.False(Try5(func() (string, string, string, string, error) {
return "", "", "", "", errors.New("foo")
}))
is.False(Try6(func() (string, string, string, string, string, error) {
return "", "", "", "", "", errors.New("foo")
}))
}
func TestTryWithErrorValue(t *testing.T) {
is := assert.New(t)
err, ok := TryWithErrorValue(func() error {
panic("error")
return nil
})
is.False(ok)
is.Equal("error", err)
err, ok = TryWithErrorValue(func() error {
return nil
})
is.True(ok)
is.Equal(nil, err)
}
func TestTryCatch(t *testing.T) {
is := assert.New(t)
caught := false
TryCatch(func() error {
panic("error")
return nil
}, func() {
//error was caught
caught = true
})
is.True(caught)
caught = false
TryCatch(func() error {
return nil
}, func() {
//no error to be caught
caught = true
})
is.False(caught)
}
func TestTryCatchWithErrorValue(t *testing.T) {
is := assert.New(t)
caught := false
TryCatchWithErrorValue(func() error {
panic("error")
return nil
}, func(val any) {
//error was caught
caught = val == "error"
})
is.True(caught)
caught = false
TryCatchWithErrorValue(func() error {
return nil
}, func(val any) {
//no error to be caught
caught = true
})
is.False(caught)
}
+11 -8
View File
@@ -1,8 +1,9 @@
package lo
import (
"github.com/stretchr/testify/assert"
"testing"
"github.com/stretchr/testify/assert"
)
func TestRange(t *testing.T) {
@@ -34,11 +35,13 @@ func TestRangeClose(t *testing.T) {
result1 := RangeWithSteps(0, 20, 6)
result2 := RangeWithSteps(0, 3, -5)
result3 := RangeWithSteps(1, 1, 0)
result4 := RangeWithSteps[float64](1.0, 4.0, 2.0)
result5 := RangeWithSteps[float32](-1.0, -4.0, -1.0)
is.Equal(result1, []int{0, 6, 12, 18})
is.Equal(result2, []int{})
is.Equal(result3, []int{})
is.Equal(result4, []float64{1.0, 3.0})
is.Equal(result5, []float32{-1.0, -2.0, -3.0})
result4 := RangeWithSteps(3, 2, 1)
result5 := RangeWithSteps[float64](1.0, 4.0, 2.0)
result6 := RangeWithSteps[float32](-1.0, -4.0, -1.0)
is.Equal([]int{0, 6, 12, 18}, result1)
is.Equal([]int{}, result2)
is.Equal([]int{}, result3)
is.Equal([]int{}, result4)
is.Equal([]float64{1.0, 3.0}, result5)
is.Equal([]float32{-1.0, -2.0, -3.0}, result6)
}