From ab2e5a3137484fcc4dcf6db815601b1ca2201c41 Mon Sep 17 00:00:00 2001 From: dudaodong Date: Tue, 3 Sep 2024 20:25:06 +0800 Subject: [PATCH] feat: add new functions --- docs/function.md | 95 ++++++++++++++++++++++++- docs/function_zh-CN.md | 93 +++++++++++++++++++++++- function/function.go | 70 ++++++++++++++++++ function/function_test.go | 144 +++++++++++++++++++++++++++++++++++++- strutil/string.go | 85 ++++++++++++++++++++++ strutil/string_test.go | 76 ++++++++++++++++++++ 6 files changed, 559 insertions(+), 4 deletions(-) diff --git a/docs/function.md b/docs/function.md index c6dfd76..22da69e 100644 --- a/docs/function.md +++ b/docs/function.md @@ -27,7 +27,9 @@ import ( - [Before](#Before) - [Curry](#Curry) - [Compose](#Compose) -- [Debounced](#Debounced) +- [Debounce](#Debounce) +- [Debounceddeprecated](#Debounced) +- [Throttle](#Throttle) - [Delay](#Delay) - [Pipeline](#Pipeline) - [Schedule](#Schedule) @@ -199,7 +201,7 @@ func main() { ### Debounced -

Creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked.

+

Creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked. This function is deprecated. use Debounce instead.

Signature: @@ -238,6 +240,53 @@ func main() { } ``` +### Debounce + +

Creates a debounced version of the provided function. The debounced function will only invoke the original function after the specified delay has passed since the last time it was invoked. It also supports canceling the debounce.

+ +Signature: + +```go +func Debounce(fn func(), delay time.Duration) (debouncedFn func(), cancelFn func()) +``` + +Example: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/function" +) + +func main() { + callCount := 0 + fn := func() { + callCount++ + } + + debouncedFn, _ := function.Debounce(fn, 500*time.Millisecond) + + for i := 0; i < 10; i++ { + debouncedFn() + time.Sleep(50 * time.Millisecond) + } + + time.Sleep(1 * time.Second) + fmt.Println(callCount) + + debouncedFn() + + time.Sleep(1 * time.Second) + fmt.Println(callCount) + + // Output: + // 1 + // 2 +} +``` + ### Delay

Invoke function after delayed time.

@@ -266,6 +315,48 @@ func main() { } ``` +### Throttle + +

Creates a throttled version of the provided function. The returned function guarantees that it will only be invoked at most once per interval.

+ +Signature: + +```go +func Throttle(fn func(), interval time.Duration) func() +``` + +Example: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/function" +) + +func main() { + callCount := 0 + + fn := func() { + callCount++ + } + + throttledFn := function.Throttle(fn, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + fmt.Println(callCount) + + // Output: + // 1 +} +``` + ### Schedule

Invoke function every duration time, until close the returned bool chan.

diff --git a/docs/function_zh-CN.md b/docs/function_zh-CN.md index a8e15bb..e9d9e10 100644 --- a/docs/function_zh-CN.md +++ b/docs/function_zh-CN.md @@ -27,7 +27,9 @@ import ( - [Before](#Before) - [Curry](#Curry) - [Compose](#Compose) -- [Debounced](#Debounced) +- [Debounce](#Debounce) +- [Debounceddeprecated](#Debounced) +- [Throttle](#Throttle) - [Delay](#Delay) - [Pipeline](#Pipeline) - [Schedule](#Schedule) @@ -197,6 +199,53 @@ func main() { } ``` +### Debounce + +

创建一个函数的去抖动版本。该去抖动函数仅在上次调用后的指定延迟时间过去之后才会调用原始函数。支持取消去抖动。

+ +函数签名: + +```go +func Debounce(fn func(), delay time.Duration) (debouncedFn func(), cancelFn func()) +``` + +示例: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/function" +) + +func main() { + callCount := 0 + fn := func() { + callCount++ + } + + debouncedFn, _ := function.Debounce(fn, 500*time.Millisecond) + + for i := 0; i < 10; i++ { + debouncedFn() + time.Sleep(50 * time.Millisecond) + } + + time.Sleep(1 * time.Second) + fmt.Println(callCount) + + debouncedFn() + + time.Sleep(1 * time.Second) + fmt.Println(callCount) + + // Output: + // 1 + // 2 +} +``` + ### Debounced

创建一个 debounced 函数,该函数延迟调用 fn 直到自上次调用 debounced 函数后等待持续时间过去。

@@ -238,6 +287,48 @@ func main() { } ``` +### Throttle + +

创建一个函数的节流版本。返回的函数保证在每个时间间隔内最多只会被调用一次。

+ +函数签名: + +```go +func Throttle(fn func(), interval time.Duration) func() +``` + +示例: + +```go +package main + +import ( + "fmt" + "github.com/duke-git/lancet/function" +) + +func main() { + callCount := 0 + + fn := func() { + callCount++ + } + + throttledFn := function.Throttle(fn, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + fmt.Println(callCount) + + // Output: + // 1 +} +``` + ### Delay

延迟delay时间后调用函数

diff --git a/function/function.go b/function/function.go index 07dcc22..50b4704 100644 --- a/function/function.go +++ b/function/function.go @@ -6,6 +6,7 @@ package function import ( "reflect" + "sync" "time" ) @@ -75,6 +76,7 @@ func Delay(delay time.Duration, fn interface{}, args ...interface{}) { } // Debounced creates a debounced function that delays invoking fn until after wait duration have elapsed since the last time the debounced function was invoked. +// Deprecated: Use Debounce function instead. func Debounced(fn func(), duration time.Duration) func() { // Catch programming error while constructing the closure mustBeFunction(fn) @@ -94,6 +96,74 @@ func Debounced(fn func(), duration time.Duration) func() { return func() { timer.Reset(duration) } } +// Debounce creates a debounced version of the provided function. +func Debounce(fn func(), delay time.Duration) (debouncedFn func(), cancelFn func()) { + var ( + timer *time.Timer + mu sync.Mutex + ) + + debouncedFn = func() { + mu.Lock() + defer mu.Unlock() + + if timer != nil { + timer.Stop() + } + + timer = time.AfterFunc(delay, func() { + fn() + }) + } + + cancelFn = func() { + mu.Lock() + defer mu.Unlock() + + if timer != nil { + timer.Stop() + } + } + + return debouncedFn, cancelFn +} + +// Throttle creates a throttled version of the provided function. +// The returned function guarantees that it will only be invoked at most once per interval. +func Throttle(fn func(), interval time.Duration) func() { + var ( + timer *time.Timer + lastRun time.Time + mu sync.Mutex + ) + + return func() { + mu.Lock() + defer mu.Unlock() + + now := time.Now() + if now.Sub(lastRun) >= interval { + fn() + lastRun = now + if timer != nil { + timer.Stop() + timer = nil + } + } else if timer == nil { + delay := interval - now.Sub(lastRun) + + timer = time.AfterFunc(delay, func() { + mu.Lock() + defer mu.Unlock() + + fn() + lastRun = time.Now() + timer = nil + }) + } + } +} + // Schedule invoke function every duration time, util close the returned bool chan func Schedule(d time.Duration, fn interface{}, args ...interface{}) chan bool { // Catch programming error while constructing the closure diff --git a/function/function_test.go b/function/function_test.go index dfd900e..aeb8546 100644 --- a/function/function_test.go +++ b/function/function_test.go @@ -43,7 +43,7 @@ func TestBefore(t *testing.T) { var res []int64 type cb func(args ...interface{}) []reflect.Value - appendStr := func(i int, s string, fn cb) { + appendStr := func(i int, _ string, fn cb) { v := fn(i) res = append(res, v[0].Int()) } @@ -148,3 +148,145 @@ func TestPipeline(t *testing.T) { assert.Equal(36, f(2)) } + +func TestDebounce(t *testing.T) { + assert := internal.NewAssert(t, "TestDebounce") + + t.Run("Single call", func(t *testing.T) { + callCount := 0 + + debouncedFn, _ := Debounce(func() { + callCount++ + }, 500*time.Millisecond) + + debouncedFn() + + time.Sleep(1 * time.Second) + + assert.Equal(1, callCount) + }) + + t.Run("Multiple calls within debounce interval", func(t *testing.T) { + callCount := 0 + + debouncedFn, _ := Debounce(func() { + callCount++ + }, 1*time.Second) + + for i := 0; i < 5; i++ { + go func(index int) { + time.Sleep(time.Duration(index) * 200 * time.Millisecond) + debouncedFn() + }(i) + } + + time.Sleep(2 * time.Second) + + assert.Equal(1, callCount) + }) + + t.Run("Immediate consecutive calls", func(t *testing.T) { + callCount := 0 + + debouncedFn, _ := Debounce(func() { + callCount++ + }, 500*time.Millisecond) + + for i := 0; i < 10; i++ { + debouncedFn() + time.Sleep(50 * time.Millisecond) + } + + time.Sleep(1 * time.Second) + + assert.Equal(1, callCount) + }) + + t.Run("Cancel calls", func(t *testing.T) { + callCount := 0 + + debouncedFn, cancelFn := Debounce(func() { + callCount++ + }, 500*time.Millisecond) + + debouncedFn() + + cancelFn() + + time.Sleep(1 * time.Second) + + assert.Equal(0, callCount) + }) + +} + +func TestThrottle(t *testing.T) { + assert := internal.NewAssert(t, "TestThrottle") + + t.Run("Single call", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + throttledFn() + + time.Sleep(100 * time.Millisecond) + + assert.Equal(1, callCount) + }) + + t.Run("Multiple calls within throttle interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + for i := 0; i < 5; i++ { + throttledFn() + } + + time.Sleep(1 * time.Second) + + assert.Equal(1, callCount) + }) + + t.Run("Mutiple calls space out throttle interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 500*time.Millisecond) + + throttledFn() + time.Sleep(600 * time.Millisecond) + + throttledFn() + time.Sleep(600 * time.Millisecond) + + throttledFn() + + time.Sleep(1 * time.Second) + + assert.Equal(3, callCount) + }) + + t.Run("Call function near the end of the interval", func(t *testing.T) { + callCount := 0 + + throttledFn := Throttle(func() { + callCount++ + }, 1*time.Second) + + throttledFn() + time.Sleep(900 * time.Millisecond) + + throttledFn() + time.Sleep(200 * time.Millisecond) + + assert.Equal(2, callCount) + }) + +} diff --git a/strutil/string.go b/strutil/string.go index 323453b..31289d1 100644 --- a/strutil/string.go +++ b/strutil/string.go @@ -8,11 +8,17 @@ import ( "errors" "regexp" "strings" + "time" "unicode" "unicode/utf8" "unsafe" + + "math/rand" ) +// used in `Shuffle` function +var rng = rand.New(rand.NewSource(int64(time.Now().UnixNano()))) + // CamelCase covert string to camelCase string. // non letters and numbers will be ignored // eg. "Foo-#1😄$_%^&*(1bar" => "foo11Bar" @@ -559,3 +565,82 @@ func HammingDistance(a, b string) (int, error) { return distance, nil } + +// Concat uses the strings.Builder to concatenate the input strings. +// - `length` is the expected length of the concatenated string. +// - if you are unsure about the length of the string to be concatenated, please pass 0 or a negative number. +// +// Play: todo +func Concat(length int, str ...string) string { + if len(str) == 0 { + return "" + } + + sb := strings.Builder{} + if length <= 0 { + sb.Grow(len(str[0]) * len(str)) + } else { + sb.Grow(length) + } + + for _, s := range str { + sb.WriteString(s) + } + return sb.String() +} + +// Ellipsis truncates a string to a specified length and appends an ellipsis. +func Ellipsis(str string, length int) string { + str = strings.TrimSpace(str) + + if length <= 0 { + return "" + } + + runes := []rune(str) + + if len(runes) <= length { + return str + } + + return string(runes[:length]) + "..." +} + +// Shuffle the order of characters of given string. +func Shuffle(str string) string { + runes := []rune(str) + + for i := len(runes) - 1; i > 0; i-- { + j := rng.Intn(i + 1) + runes[i], runes[j] = runes[j], runes[i] + } + + return string(runes) +} + +// Rotate rotates the string by the specified number of characters. +func Rotate(str string, shift int) string { + if shift == 0 { + return str + } + + runes := []rune(str) + length := len(runes) + if length == 0 { + return str + } + + shift = shift % length + + if shift < 0 { + shift = length + shift + } + + var sb strings.Builder + sb.Grow(length) + + sb.WriteString(string(runes[length-shift:])) + sb.WriteString(string(runes[:length-shift])) + + return sb.String() +} diff --git a/strutil/string_test.go b/strutil/string_test.go index ab14791..ab50fa3 100644 --- a/strutil/string_test.go +++ b/strutil/string_test.go @@ -505,3 +505,79 @@ func TestSubInBetween(t *testing.T) { assert.Equal("", SubInBetween(str, "a", "")) assert.Equal("", SubInBetween(str, "a", "f")) } + +func TestConcat(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestConcat") + + assert.Equal("", Concat(0)) + assert.Equal("a", Concat(1, "a")) + assert.Equal("ab", Concat(2, "a", "b")) + assert.Equal("abc", Concat(3, "a", "b", "c")) + assert.Equal("abc", Concat(3, "a", "", "b", "c", "")) + assert.Equal("你好,世界!", Concat(0, "你好", ",", "", "世界!", "")) + assert.Equal("Hello World!", Concat(0, "Hello", " Wo", "r", "ld!", "")) +} + +func TestEllipsis(t *testing.T) { + t.Parallel() + assert := internal.NewAssert(t, "TestEllipsis") + + tests := []struct { + input string + length int + expected string + }{ + {"", 0, ""}, + {"hello world", 0, ""}, + {"hello world", -1, ""}, + {"hello world", 5, "hello..."}, + {"hello world", 11, "hello world"}, + {"你好,世界!", 2, "你好..."}, + {"😀😃😄😁😆", 3, "😀😃😄..."}, + {"This is a test.", 10, "This is a ..."}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, Ellipsis(tt.input, tt.length)) + } +} + +func TestShuffle(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestShuffle") + + assert.Equal("", Shuffle("")) + assert.Equal("a", Shuffle("a")) + + str := "hello" + shuffledStr := Shuffle(str) + assert.Equal(5, len(shuffledStr)) +} + +func TestRotate(t *testing.T) { + t.Parallel() + + assert := internal.NewAssert(t, "TestRotate") + + tests := []struct { + input string + shift int + expected string + }{ + {"", 1, ""}, + {"a", 0, "a"}, + {"a", 1, "a"}, + {"a", -1, "a"}, + + {"Hello", -2, "lloHe"}, + {"Hello", 1, "oHell"}, + {"Hello, world!", 3, "ld!Hello, wor"}, + } + + for _, tt := range tests { + assert.Equal(tt.expected, Rotate(tt.input, tt.shift)) + } +}