diff --git a/.github/PULL_REQUEST_TEMPLATE/config.yml b/.github/PULL_REQUEST_TEMPLATE/config.yml new file mode 100644 index 00000000..36bbc5ee --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +template_chooser: + enabled: true + default: "other.md" + choices: + - name: "New Helper" + file: "new_helper.md" + - name: "Other Changes" + file: "other.md" diff --git a/.github/PULL_REQUEST_TEMPLATE/new_helper.md b/.github/PULL_REQUEST_TEMPLATE/new_helper.md new file mode 100644 index 00000000..341ab35e --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/new_helper.md @@ -0,0 +1,23 @@ + +## Describe your changes + +... + +## Checklist before requesting a review + +- [ ] πŸ‘“ I have performed a self-review of my code +- [ ] πŸ‘Ά This helper does not already exist +- [ ] πŸ§ͺ This helper is tested +- [ ] 🏎️ My code limits memory allocation and is fast +- [ ] πŸ§žβ€β™‚οΈ This helper is immutable and my tests prove it +- [ ] ✍️ I implemented the parallel and mutable variants +- [ ] πŸ“– My helper has been added to README +- [ ] πŸ”¬ An example has been added to xxxxx_example_test.go +- [ ] ⛹️ An example has been created on https://go.dev/play + +## Conventions + +- Returning `(ok bool)` is often better than `(err error)` +- `panic(...)` must be limited +- Helpers should allow batching (eg: receive variadic arguments) +- Use an index at the end of the helper name to declare variants (eg: `lo.Must0`, `lo.Must1`, `lo.Must2`...) diff --git a/.github/PULL_REQUEST_TEMPLATE/other.md b/.github/PULL_REQUEST_TEMPLATE/other.md new file mode 100644 index 00000000..e69de29b diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 8b9e4e11..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,434 +0,0 @@ -# Changelog - -@samber: I sometimes forget to update this file. Ping me on [Twitter](https://twitter.com/samuelberthe) or open an issue in case of error. We need to keep a clear changelog for easier lib upgrade. - -## 1.39.0 (2023-12-01) - -Improvement: -- Adding IsNil - -## 1.38.1 (2023-03-20) - -Improvement: -- Async and AsyncX: now returns `<-chan T` instead of `chan T` - -## 1.38.0 (2023-03-20) - -Adding: -- lo.ValueOr -- lo.DebounceBy -- lo.EmptyableToPtr - -Improvement: -- Substring: add support for non-English chars - -Fix: -- Async: Fix goroutine leak - -## 1.37.0 (2022-12-15) - -Adding: -- lo.PartialX -- lo.Transaction - -Improvement: -- lo.Associate / lo.SliceToMap: faster memory allocation - -Chore: -- Remove *_test.go files from releases, in order to cleanup dev dependencies - -## 1.36.0 (2022-11-28) - -Adding: -- lo.AttemptWhile -- lo.AttemptWhileWithDelay - -## 1.35.0 (2022-11-15) - -Adding: -- lo.RandomString -- lo.BufferWithTimeout (alias to lo.BatchWithTimeout) -- lo.Buffer (alias to lo.Batch) - -Change: -- lo.Slice: avoid panic caused by out-of-bounds - -Deprecation: -- lo.BatchWithTimeout -- lo.Batch - -## 1.34.0 (2022-11-12) - -Improving: -- lo.Union: faster and can receive more than 2 lists - -Adding: -- lo.FanIn (alias to lo.ChannelMerge) -- lo.FanOut - -Deprecation: -- lo.ChannelMerge - -## 1.33.0 (2022-10-14) - -Adding: -- lo.ChannelMerge - -Improving: -- helpers with callbacks/predicates/iteratee now have named arguments, for easier autocompletion - -## 1.32.0 (2022-10-10) - -Adding: - -- lo.ChannelToSlice -- lo.CountValues -- lo.CountValuesBy -- lo.MapEntries -- lo.Sum -- lo.Interleave -- TupleX.Unpack() - -## 1.31.0 (2022-10-06) - -Adding: - -- lo.SliceToChannel -- lo.Generator -- lo.Batch -- lo.BatchWithTimeout - -## 1.30.1 (2022-10-06) - -Fix: - -- lo.Try1: remove generic type -- lo.Validate: format error properly - -## 1.30.0 (2022-10-04) - -Adding: - -- lo.TernaryF -- lo.Validate - -## 1.29.0 (2022-10-02) - -Adding: - -- lo.ErrorAs -- lo.TryOr -- lo.TryOrX - -## 1.28.0 (2022-09-05) - -Adding: - -- lo.ChannelDispatcher with 6 dispatching strategies: - - lo.DispatchingStrategyRoundRobin - - lo.DispatchingStrategyRandom - - lo.DispatchingStrategyWeightedRandom - - lo.DispatchingStrategyFirst - - lo.DispatchingStrategyLeast - - lo.DispatchingStrategyMost - -## 1.27.1 (2022-08-15) - -Bugfix: - -- Removed comparable constraint for lo.FindKeyBy - -## 1.27.0 (2022-07-29) - -Breaking: - -- Change of MapToSlice prototype: `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(V, K) R) []R` -> `MapToSlice[K comparable, V any, R any](in map[K]V, iteratee func(K, V) R) []R` - -Added: - -- lo.ChunkString -- lo.SliceToMap (alias to lo.Associate) - -## 1.26.0 (2022-07-24) - -Adding: - -- lo.Associate -- lo.ReduceRight -- lo.FromPtrOr -- lo.MapToSlice -- lo.IsSorted -- lo.IsSortedByKey - -## 1.25.0 (2022-07-04) - -Adding: - -- lo.FindUniques -- lo.FindUniquesBy -- lo.FindDuplicates -- lo.FindDuplicatesBy -- lo.IsNotEmpty - -## 1.24.0 (2022-07-04) - -Adding: - -- lo.Without -- lo.WithoutEmpty - -## 1.23.0 (2022-07-04) - -Adding: - -- lo.FindKey -- lo.FindKeyBy - -## 1.22.0 (2022-07-04) - -Adding: - -- lo.Slice -- lo.FromPtr -- lo.IsEmpty -- lo.Compact -- lo.ToPairs: alias to lo.Entries -- lo.FromPairs: alias to lo.FromEntries -- lo.Partial - -Change: - -- lo.Must + lo.MustX: add context to panic message - -Fix: - -- lo.Nth: out of bound exception (#137) - -## 1.21.0 (2022-05-10) - -Adding: - -- lo.ToAnySlice -- lo.FromAnySlice - -## 1.20.0 (2022-05-02) - -Adding: - -- lo.Synchronize -- lo.SumBy - -Change: -- Removed generic type definition for lo.Try0: `lo.Try0[T]()` -> `lo.Try0()` - -## 1.19.0 (2022-04-30) - -Adding: - -- lo.RepeatBy -- lo.Subset -- lo.Replace -- lo.ReplaceAll -- lo.Substring -- lo.RuneLength - -## 1.18.0 (2022-04-28) - -Adding: - -- lo.SomeBy -- lo.EveryBy -- lo.None -- lo.NoneBy - -## 1.17.0 (2022-04-27) - -Adding: - -- lo.Unpack2 -> lo.Unpack3 -- lo.Async0 -> lo.Async6 - -## 1.16.0 (2022-04-26) - -Adding: - -- lo.AttemptWithDelay - -## 1.15.0 (2022-04-22) - -Improvement: - -- lo.Must: error or boolean value - -## 1.14.0 (2022-04-21) - -Adding: - -- lo.Coalesce - -## 1.13.0 (2022-04-14) - -Adding: - -- PickBy -- PickByKeys -- PickByValues -- OmitBy -- OmitByKeys -- OmitByValues -- Clamp -- MapKeys -- Invert -- IfF + ElseIfF + ElseF -- T0() + T1() + T2() + T3() + ... - -## 1.12.0 (2022-04-12) - -Adding: - -- Must -- Must{0-6} -- FindOrElse -- Async -- MinBy -- MaxBy -- Count -- CountBy -- FindIndexOf -- FindLastIndexOf -- FilterMap - -## 1.11.0 (2022-03-11) - -Adding: - -- Try -- Try{0-6} -- TryWitchValue -- TryCatch -- TryCatchWitchValue -- Debounce -- Reject - -## 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.2.0 (2022-03-03) - -Adding `lop.Map` and `lop.ForEach`. - -## 1.1.0 (2022-03-03) - -Adding `i int` param to `lo.Map()`, `lo.Filter()`, `lo.ForEach()` and `lo.Reduce()` predicates. - -## 1.0.0 (2022-03-02) - -*Initial release* - -Supported helpers for slices: - -- Filter -- Map -- Reduce -- ForEach -- Uniq -- UniqBy -- GroupBy -- Chunk -- Flatten -- Shuffle -- Reverse -- Fill -- ToMap - -Supported helpers for maps: - -- Keys -- Values -- Entries -- FromEntries -- Assign (maps merge) - -Supported intersection helpers: - -- Contains -- Every -- Some -- Intersect -- Difference - -Supported search helpers: - -- IndexOf -- LastIndexOf -- Find -- Min -- Max -- Last -- Nth - -Other functional programming helpers: - -- Ternary (1 line if/else statement) -- If / ElseIf / Else -- Switch / Case / Default -- ToPtr -- ToSlicePtr - -Constraints: - -- Clonable diff --git a/Dockerfile b/Dockerfile index bd01bbbb..5dbeb415 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM golang:1.18 +FROM golang:1.23.1 WORKDIR /go/src/github.com/samber/lo diff --git a/README.md b/README.md index 9fc83147..22b710f1 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Supported helpers for slices: - [Reduce](#reduce) - [ReduceRight](#reduceright) - [ForEach](#foreach) +- [ForEachWhile](#foreachwhile) - [Times](#times) - [Uniq](#uniq) - [UniqBy](#uniqby) @@ -104,6 +105,7 @@ Supported helpers for slices: - [DropRight](#dropright) - [DropWhile](#dropwhile) - [DropRightWhile](#droprightwhile) +- [DropByIndex](#DropByIndex) - [Reject](#reject) - [RejectMap](#rejectmap) - [FilterReject](#filterreject) @@ -118,12 +120,16 @@ Supported helpers for slices: - [Compact](#compact) - [IsSorted](#issorted) - [IsSortedByKey](#issortedbykey) +- [Splice](#Splice) Supported helpers for maps: - [Keys](#keys) +- [UniqKeys](#uniqkeys) +- [HasKey](#haskey) - [ValueOr](#valueor) - [Values](#values) +- [UniqValues](#uniqvalues) - [PickBy](#pickby) - [PickByKeys](#pickbykeys) - [PickByValues](#pickbyvalues) @@ -160,7 +166,7 @@ Supported helpers for strings: - [SnakeCase](#snakecase) - [Words](#words) - [Capitalize](#capitalize) -- [Elipse](#elipse) +- [Ellipsis](#ellipsis) Supported helpers for tuples: @@ -174,7 +180,7 @@ Supported helpers for tuples: Supported helpers for time and duration: - [Duration](#duration) -- [Duration0 -> Duration10](#duration0-duration10) +- [Duration0 -> Duration10](#duration0---duration10) Supported helpers for channels: @@ -219,9 +225,11 @@ Supported search helpers: - [Min](#min) - [MinBy](#minby) - [Earliest](#earliest) +- [EarliestBy](#earliestby) - [Max](#max) - [MaxBy](#maxby) - [Latest](#latest) +- [LatestBy](#latestby) - [First](#first) - [FirstOrEmpty](#FirstOrEmpty) - [FirstOr](#FirstOr) @@ -248,6 +256,8 @@ Type manipulation helpers: - [FromPtr](#fromptr) - [FromPtrOr](#fromptror) - [ToSlicePtr](#tosliceptr) +- [FromSlicePtr](#fromsliceptr) +- [FromSlicePtrOr](#fromsliceptror) - [ToAnySlice](#toanyslice) - [FromAnySlice](#fromanyslice) - [Empty](#empty) @@ -272,6 +282,8 @@ Concurrency helpers: - [Synchronize](#synchronize) - [Async](#async) - [Transaction](#transaction) +- [WaitFor](#waitfor) +- [WaitForWithContext](#waitforwithcontext) Error handling: @@ -415,6 +427,26 @@ lop.ForEach([]string{"hello", "world"}, func(x string, _ int) { // prints "hello\nworld\n" or "world\nhello\n" ``` +### ForEachWhile + +Iterates over collection elements and invokes iteratee for each element collection return value decide to continue or break, like do while(). + +```go +list := []int64{1, 2, -42, 4} + +lo.ForEachWhile(list, func(x int64, _ int) bool { + if x < 0 { + return false + } + fmt.Println(x) + return true +}) +// 1 +// 2 +``` + +[[play](https://go.dev/play/p/QnLGt35tnow)] + ### Times Times invokes the iteratee n times, returning an array of the results of each invocation. The iteratee is invoked with index as argument. @@ -744,6 +776,18 @@ l := lo.DropRightWhile([]string{"a", "aa", "aaa", "aa", "aa"}, func(val string) [[play](https://go.dev/play/p/3-n71oEC0Hz)] +### DropByIndex + +Drops elements from a slice or array by the index. A negative index will drop elements from the end of the slice. + +```go +l := lo.DropByIndex([]int{0, 1, 2, 3, 4, 5}, 2, 4, -1) +// []int{0, 1, 3} +``` + +[[play](https://go.dev/play/p/JswS7vXRJP2)] + + ### Reject The opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. @@ -978,28 +1022,108 @@ slice := lo.IsSortedByKey([]string{"a", "bb", "ccc"}, func(s string) int { [[play](https://go.dev/play/p/wiG6XyBBu49)] +### Splice + +Splice inserts multiple elements at index i. A negative index counts back from the end of the slice. The helper is protected against overflow errors. + +```go +result := lo.Splice([]string{"a", "b"}, 1, "1", "2") +// []string{"a", "1", "2", "b"} + +// negative +result = lo.Splice([]string{"a", "b"}, -1, "1", "2") +// []string{"a", "1", "2", "b"} + +// overflow +result = lo.Splice([]string{"a", "b"}, 42, "1", "2") +// []string{"a", "b", "1", "2"} +``` + +[[play](https://go.dev/play/p/G5_GhkeSUBA)] + ### Keys -Creates an array of the map keys. +Creates a slice of the map keys. + +Use the UniqKeys variant to deduplicate common keys. ```go keys := lo.Keys(map[string]int{"foo": 1, "bar": 2}) // []string{"foo", "bar"} + +keys := lo.Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) +// []string{"foo", "bar", "baz"} + +keys := lo.Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 3}) +// []string{"foo", "bar", "bar"} ``` [[play](https://go.dev/play/p/Uu11fHASqrU)] +### UniqKeys + +Creates an array of unique map keys. + +```go +keys := lo.UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) +// []string{"foo", "bar", "baz"} + +keys := lo.UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 3}) +// []string{"foo", "bar"} +``` + +[[play](https://go.dev/play/p/TPKAb6ILdHk)] + +### HasKey + +Returns whether the given key exists. + +```go +exists := lo.HasKey(map[string]int{"foo": 1, "bar": 2}, "foo") +// true + +exists := lo.HasKey(map[string]int{"foo": 1, "bar": 2}, "baz") +// false +``` + +[[play](https://go.dev/play/p/aVwubIvECqS)] + ### Values Creates an array of the map values. +Use the UniqValues variant to deduplicate common values. + ```go values := lo.Values(map[string]int{"foo": 1, "bar": 2}) // []int{1, 2} + +values := lo.Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) +// []int{1, 2, 3} + +values := lo.Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 2}) +// []int{1, 2, 2} ``` [[play](https://go.dev/play/p/nnRTQkzQfF6)] +### UniqValues + +Creates an array of unique map values. + +```go +values := lo.UniqValues(map[string]int{"foo": 1, "bar": 2}) +// []int{1, 2} + +values := lo.UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) +// []int{1, 2, 3} + +values := lo.UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 2}) +// []int{1, 2} +``` + +[[play](https://go.dev/play/p/nf6bXMh7rM3)] + ### ValueOr Returns the value of the given key or the fallback value if the key is not present. @@ -1451,18 +1575,18 @@ str := lo.Capitalize("heLLO") // Hello ``` -### Elipse +### Ellipsis -Truncates a string to a specified length and appends an ellipsis if truncated. +Trims and truncates a string to a specified length and appends an ellipsis if truncated. ```go -str := lo.Elipse("Lorem Ipsum", 5) +str := lo.Ellipsis(" Lorem Ipsum ", 5) // Lo... -str := lo.Elipse("Lorem Ipsum", 100) +str := lo.Ellipsis("Lorem Ipsum", 100) // Lorem Ipsum -str := lo.Elipse("Lorem Ipsum", 3) +str := lo.Ellipsis("Lorem Ipsum", 3) // ... ``` @@ -1578,7 +1702,7 @@ err, duration := lo.Duration1(func() error { // an error // 3s -err, duration := lo.Duration3(func() (string, int, error) { +str, nbr, err, duration := lo.Duration3(func() (string, int, error) { // very long job return "hello", 42, nil }) @@ -1866,7 +1990,7 @@ ok := lo.Every([]int{0, 1, 2, 3, 4, 5}, []int{0, 6}) ### EveryBy -Returns true if the predicate returns true for all of the elements in the collection or if the collection is empty. +Returns true if the predicate returns true for all elements in the collection or if the collection is empty. ```go b := EveryBy([]int{1, 2, 3, 4}, func(x int) bool { @@ -1881,7 +2005,7 @@ Returns true if at least 1 element of a subset is contained into a collection. If the subset is empty Some returns false. ```go -ok := lo.Some([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +ok := lo.Some([]int{0, 1, 2, 3, 4, 5}, []int{0, 6}) // true ok := lo.Some([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) @@ -2192,6 +2316,23 @@ earliest := lo.Earliest(time.Now(), time.Time{}) // 0001-01-01 00:00:00 +0000 UTC ``` +### EarliestBy + +Search the minimum time.Time of a collection using the given iteratee function. + +Returns zero value when the collection is empty. + +```go +type foo struct { + bar time.Time +} + +earliest := lo.EarliestBy([]foo{{time.Now()}, {}}, func(i foo) time.Time { + return i.bar +}) +// {bar:{2023-04-01 01:02:03 +0000 UTC}} +``` + ### Max Search the maximum value of a collection. @@ -2240,6 +2381,23 @@ latest := lo.Latest([]time.Time{time.Now(), time.Time{}}) // 2023-04-01 01:02:03 +0000 UTC ``` +### LatestBy + +Search the maximum time.Time of a collection using the given iteratee function. + +Returns zero value when the collection is empty. + +```go +type foo struct { + bar time.Time +} + +latest := lo.LatestBy([]foo{{time.Now()}, {}}, func(i foo) time.Time { + return i.bar +}) +// {bar:{2023-04-01 01:02:03 +0000 UTC}} +``` + ### First Returns the first element of a collection and check for availability of the first element. @@ -2485,19 +2643,19 @@ Checks if a value is nil or if it's a reference type with a nil underlying value ```go var x int -IsNil(x)) +lo.IsNil(x) // false var k struct{} -IsNil(k) +lo.IsNil(k) // false var i *int -IsNil(i) +lo.IsNil(i) // true var ifaceWithNilValue any = (*string)(nil) -IsNil(ifaceWithNilValue) +lo.IsNil(ifaceWithNilValue) // true ifaceWithNilValue == nil // false @@ -2575,6 +2733,36 @@ ptr := lo.ToSlicePtr([]string{"hello", "world"}) // []*string{"hello", "world"} ``` +### FromSlicePtr + +Returns a slice with the pointer values. +Returns a zero value in case of a nil pointer element. + +```go +str1 := "hello" +str2 := "world" + +ptr := lo.FromSlicePtr[string]([]*string{&str1, &str2, nil}) +// []string{"hello", "world", ""} + +ptr := lo.Compact( + lo.FromSlicePtr[string]([]*string{&str1, &str2, nil}), +) +// []string{"hello", "world"} +``` + +### FromSlicePtrOr + +Returns a slice with the pointer values or the fallback value. + +```go +str1 := "hello" +str2 := "world" + +ptr := lo.FromSlicePtrOr[string]([]*string{&str1, &str2, "fallback value"}) +// []string{"hello", "world", "fallback value"} +``` + ### ToAnySlice Returns a slice with all elements mapped to `any` type. @@ -2725,7 +2913,9 @@ f(42, -4) ### 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 successful response is returned. +Invokes a function N times until it returns valid output. Returns either the caught error or nil. + +When the first argument is less than `1`, the function runs until a successful response is returned. ```go iter, err := lo.Attempt(42, func(i int) error { @@ -2765,9 +2955,9 @@ For more advanced retry strategies (delay, exponential backoff...), please take ### AttemptWithDelay -Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil. +Invokes a function N times until it returns valid output, with a pause between each call. Returns either the caught error or nil. -When first argument is less than `1`, the function runs until a successful response is returned. +When the first argument is less than `1`, the function runs until a successful response is returned. ```go iter, duration, err := lo.AttemptWithDelay(5, 2*time.Second, func(i int, duration time.Duration) error { @@ -2788,9 +2978,9 @@ For more advanced retry strategies (delay, exponential backoff...), please take ### AttemptWhile -Invokes a function N times until it returns valid output. Returning either the caught error or nil, and along with a bool value to identifying whether it needs invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value. +Invokes a function N times until it returns valid output. Returns either the caught error or nil, along with a bool value to determine whether the function should be invoked again. It will terminate the invoke immediately if the second return value is false. -When first argument is less than `1`, the function runs until a successful response is returned. +When the first argument is less than `1`, the function runs until a successful response is returned. ```go count1, err1 := lo.AttemptWhile(5, func(i int) (error, bool) { @@ -2813,9 +3003,9 @@ For more advanced retry strategies (delay, exponential backoff...), please take ### AttemptWhileWithDelay -Invokes a function N times until it returns valid output, with a pause between each call. Returning either the caught error or nil, and along with a bool value to identifying whether it needs to invoke function continuously. It will terminate the invoke immediately if second bool value is returned with falsy value. +Invokes a function N times until it returns valid output, with a pause between each call. Returns either the caught error or nil, along with a bool value to determine whether the function should be invoked again. It will terminate the invoke immediately if the second return value is false. -When first argument is less than `1`, the function runs until a successful response is returned. +When the first argument is less than `1`, the function runs until a successful response is returned. ```go count1, time1, err1 := lo.AttemptWhileWithDelay(5, time.Millisecond, func(i int, d time.Duration) (error, bool) { @@ -2989,6 +3179,81 @@ _, _ = transaction.Process(-5) // rollback 1 ``` +### WaitFor + +Runs periodically until a condition is validated. + +```go +alwaysTrue := func(i int) bool { return true } +alwaysFalse := func(i int) bool { return false } +laterTrue := func(i int) bool { + return i > 5 +} + +iterations, duration, ok := lo.WaitFor(alwaysTrue, 10*time.Millisecond, 2 * time.Millisecond) +// 1 +// 1ms +// true + +iterations, duration, ok := lo.WaitFor(alwaysFalse, 10*time.Millisecond, time.Millisecond) +// 10 +// 10ms +// false + +iterations, duration, ok := lo.WaitFor(laterTrue, 10*time.Millisecond, time.Millisecond) +// 7 +// 7ms +// true + +iterations, duration, ok := lo.WaitFor(laterTrue, 10*time.Millisecond, 5*time.Millisecond) +// 2 +// 10ms +// false +``` + + +### WaitForWithContext + +Runs periodically until a condition is validated or context is invalid. + +The condition receives also the context, so it can invalidate the process in the condition checker + +```go +ctx := context.Background() + +alwaysTrue := func(_ context.Context, i int) bool { return true } +alwaysFalse := func(_ context.Context, i int) bool { return false } +laterTrue := func(_ context.Context, i int) bool { + return i >= 5 +} + +iterations, duration, ok := lo.WaitForWithContext(ctx, alwaysTrue, 10*time.Millisecond, 2 * time.Millisecond) +// 1 +// 1ms +// true + +iterations, duration, ok := lo.WaitForWithContext(ctx, alwaysFalse, 10*time.Millisecond, time.Millisecond) +// 10 +// 10ms +// false + +iterations, duration, ok := lo.WaitForWithContext(ctx, laterTrue, 10*time.Millisecond, time.Millisecond) +// 5 +// 5ms +// true + +iterations, duration, ok := lo.WaitForWithContext(ctx, laterTrue, 10*time.Millisecond, 5*time.Millisecond) +// 2 +// 10ms +// false + +expiringCtx, cancel := context.WithTimeout(ctx, 5*time.Millisecond) +iterations, duration, ok := lo.WaitForWithContext(expiringCtx, alwaysFalse, 100*time.Millisecond, time.Millisecond) +// 5 +// 5.1ms +// false +``` + ### Validate Helper function that creates an error when a condition is not met. @@ -3229,7 +3494,7 @@ if rateLimitErr, ok := lo.ErrorsAs[*RateLimitError](err); ok { We executed a simple benchmark with a dead-simple `lo.Map` loop: -See the full implementation [here](./benchmark_test.go). +See the full implementation [here](./map_benchmark_test.go). ```go _ = lo.Map[int64](arr, func(x int64, i int) string { diff --git a/benchmark_test.go b/benchmark_test.go deleted file mode 100644 index 97b7389e..00000000 --- a/benchmark_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package lo - -import ( - "math/rand" - "strconv" - "testing" - "time" - - lop "github.com/samber/lo/parallel" - "github.com/thoas/go-funk" -) - -func sliceGenerator(size uint) []int64 { - r := rand.New(rand.NewSource(time.Now().Unix())) - - result := make([]int64, size) - - for i := uint(0); i < size; i++ { - result[i] = r.Int63() - } - - return result -} - -func BenchmarkMap(b *testing.B) { - arr := sliceGenerator(1000000) - - b.Run("lo.Map", func(b *testing.B) { - for n := 0; n < b.N; n++ { - _ = Map(arr, func(x int64, i int) string { - return strconv.FormatInt(x, 10) - }) - } - }) - - b.Run("lop.Map", func(b *testing.B) { - for n := 0; n < b.N; n++ { - _ = lop.Map(arr, func(x int64, i int) string { - return strconv.FormatInt(x, 10) - }) - } - }) - - b.Run("reflect", func(b *testing.B) { - for n := 0; n < b.N; n++ { - _ = funk.Map(arr, func(x int64) string { - return strconv.FormatInt(x, 10) - }) - } - }) - - b.Run("for", func(b *testing.B) { - for n := 0; n < b.N; n++ { - results := make([]string, len(arr)) - - for i, item := range arr { - result := strconv.FormatInt(item, 10) - results[i] = result - } - } - }) -} diff --git a/channel.go b/channel.go index 2ffdb381..228705ae 100644 --- a/channel.go +++ b/channel.go @@ -1,9 +1,10 @@ package lo import ( - "math/rand" "sync" "time" + + "github.com/samber/lo/internal/rand" ) type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int @@ -86,9 +87,7 @@ func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan // If the channel capacity is exceeded, another random channel will be selected and so on. func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int { for { - // @TODO: Upgrade to math/rand/v2 as soon as we set the minimum Go version to 1.22. - // bearer:disable go_gosec_crypto_weak_random - i := rand.Intn(len(channels)) + i := rand.IntN(len(channels)) if channelIsNotFull(channels[i]) { return i } @@ -110,9 +109,7 @@ func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy return func(msg T, index uint64, channels []<-chan T) int { for { - // @TODO: Upgrade to math/rand/v2 as soon as we set the minimum Go version to 1.22. - // bearer:disable go_gosec_crypto_weak_random - i := seq[rand.Intn(len(seq))] + i := seq[rand.IntN(len(seq))] if channelIsNotFull(channels[i]) { return i } diff --git a/concurrency.go b/concurrency.go index 31a62dde..a2ebbce2 100644 --- a/concurrency.go +++ b/concurrency.go @@ -1,6 +1,10 @@ package lo -import "sync" +import ( + "context" + "sync" + "time" +) type synchronize struct { locker sync.Locker @@ -93,3 +97,40 @@ func Async6[A, B, C, D, E, F any](f func() (A, B, C, D, E, F)) <-chan Tuple6[A, }() return ch } + +// WaitFor runs periodically until a condition is validated. +func WaitFor(condition func(i int) bool, timeout time.Duration, heartbeatDelay time.Duration) (totalIterations int, elapsed time.Duration, conditionFound bool) { + conditionWithContext := func(_ context.Context, currentIteration int) bool { + return condition(currentIteration) + } + return WaitForWithContext(context.Background(), conditionWithContext, timeout, heartbeatDelay) +} + +// WaitForWithContext runs periodically until a condition is validated or context is canceled. +func WaitForWithContext(ctx context.Context, condition func(ctx context.Context, currentIteration int) bool, timeout time.Duration, heartbeatDelay time.Duration) (totalIterations int, elapsed time.Duration, conditionFound bool) { + start := time.Now() + + if ctx.Err() != nil { + return totalIterations, time.Since(start), false + } + + ctx, cleanCtx := context.WithTimeout(ctx, timeout) + ticker := time.NewTicker(heartbeatDelay) + + defer func() { + cleanCtx() + ticker.Stop() + }() + + for { + select { + case <-ctx.Done(): + return totalIterations, time.Since(start), false + case <-ticker.C: + totalIterations++ + if condition(ctx, totalIterations-1) { + return totalIterations, time.Since(start), true + } + } + } +} diff --git a/concurrency_test.go b/concurrency_test.go index ae65efdd..61f3dd61 100644 --- a/concurrency_test.go +++ b/concurrency_test.go @@ -1,6 +1,7 @@ package lo import ( + "context" "sync" "testing" "time" @@ -212,3 +213,201 @@ func TestAsyncX(t *testing.T) { } } } + +func TestWaitFor(t *testing.T) { + t.Parallel() + + testTimeout := 100 * time.Millisecond + longTimeout := 2 * testTimeout + shortTimeout := 4 * time.Millisecond + + t.Run("exist condition works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + laterTrue := func(i int) bool { + return i >= 5 + } + + iter, duration, ok := WaitFor(laterTrue, longTimeout, time.Millisecond) + is.Equal(6, iter, "unexpected iteration count") + is.InEpsilon(6*time.Millisecond, duration, float64(500*time.Microsecond)) + is.True(ok) + }) + + t.Run("counter is incremented", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + counter := 0 + alwaysFalse := func(i int) bool { + is.Equal(counter, i) + counter++ + return false + } + + iter, duration, ok := WaitFor(alwaysFalse, shortTimeout, 1050*time.Microsecond) + is.Equal(counter, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + alwaysTrue := func(_ int) bool { return true } + alwaysFalse := func(_ int) bool { return false } + + t.Run("short timeout works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + iter, duration, ok := WaitFor(alwaysFalse, shortTimeout, 10*time.Millisecond) + is.Equal(0, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + t.Run("timeout works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + shortTimeout := 4 * time.Millisecond + iter, duration, ok := WaitFor(alwaysFalse, shortTimeout, 10*time.Millisecond) + is.Equal(0, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + t.Run("exist on first condition", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + iter, duration, ok := WaitFor(alwaysTrue, 10*time.Millisecond, time.Millisecond) + is.Equal(1, iter, "unexpected iteration count") + is.InEpsilon(time.Millisecond, duration, float64(5*time.Microsecond)) + is.True(ok) + }) +} + +func TestWaitForWithContext(t *testing.T) { + t.Parallel() + + testTimeout := 100 * time.Millisecond + longTimeout := 2 * testTimeout + shortTimeout := 4 * time.Millisecond + + t.Run("exist condition works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + laterTrue := func(_ context.Context, i int) bool { + return i >= 5 + } + + iter, duration, ok := WaitForWithContext(context.Background(), laterTrue, longTimeout, time.Millisecond) + is.Equal(6, iter, "unexpected iteration count") + is.InEpsilon(6*time.Millisecond, duration, float64(500*time.Microsecond)) + is.True(ok) + }) + + t.Run("counter is incremented", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + counter := 0 + alwaysFalse := func(_ context.Context, i int) bool { + is.Equal(counter, i) + counter++ + return false + } + + iter, duration, ok := WaitForWithContext(context.Background(), alwaysFalse, shortTimeout, 1050*time.Microsecond) + is.Equal(counter, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + alwaysTrue := func(_ context.Context, _ int) bool { return true } + alwaysFalse := func(_ context.Context, _ int) bool { return false } + + t.Run("short timeout works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + iter, duration, ok := WaitForWithContext(context.Background(), alwaysFalse, shortTimeout, 10*time.Millisecond) + is.Equal(0, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + t.Run("timeout works", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + shortTimeout := 4 * time.Millisecond + iter, duration, ok := WaitForWithContext(context.Background(), alwaysFalse, shortTimeout, 10*time.Millisecond) + is.Equal(0, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + t.Run("exist on first condition", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + iter, duration, ok := WaitForWithContext(context.Background(), alwaysTrue, 10*time.Millisecond, time.Millisecond) + is.Equal(1, iter, "unexpected iteration count") + is.InEpsilon(time.Millisecond, duration, float64(5*time.Microsecond)) + is.True(ok) + }) + + t.Run("context cancellation stops everything", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + expiringCtx, clean := context.WithTimeout(context.Background(), 8*time.Millisecond) + t.Cleanup(func() { + clean() + }) + + iter, duration, ok := WaitForWithContext(expiringCtx, alwaysFalse, 100*time.Millisecond, 3*time.Millisecond) + is.Equal(2, iter, "unexpected iteration count") + is.InEpsilon(10*time.Millisecond, duration, float64(500*time.Microsecond)) + is.False(ok) + }) + + t.Run("canceled context stops everything", func(t *testing.T) { + t.Parallel() + + testWithTimeout(t, testTimeout) + is := assert.New(t) + + canceledCtx, cancel := context.WithCancel(context.Background()) + cancel() + + iter, duration, ok := WaitForWithContext(canceledCtx, alwaysFalse, 100*time.Millisecond, 1050*time.Microsecond) + is.Equal(0, iter, "unexpected iteration count") + is.InEpsilon(1*time.Millisecond, duration, float64(5*time.Microsecond)) + is.False(ok) + }) +} diff --git a/condition_example_test.go b/condition_example_test.go index 1700967e..340095d6 100644 --- a/condition_example_test.go +++ b/condition_example_test.go @@ -98,7 +98,7 @@ func ExampleIfF() { // 3 } -func ExampleifElse_ElseIf() { +func Example_ifElse_ElseIf() { result1 := If(true, 1). ElseIf(false, 2). Else(3) @@ -138,7 +138,7 @@ func ExampleifElse_ElseIf() { // 3 } -func ExampleifElse_ElseIfF() { +func Example_ifElse_ElseIfF() { result1 := If(true, 1). ElseIf(false, 2). Else(3) @@ -178,7 +178,7 @@ func ExampleifElse_ElseIfF() { // 3 } -func ExampleifElse_Else() { +func Example_ifElse_Else() { result1 := If(true, 1). ElseIf(false, 2). Else(3) @@ -218,7 +218,7 @@ func ExampleifElse_Else() { // 3 } -func ExampleifElse_ElseF() { +func Example_ifElse_ElseF() { result1 := If(true, 1). ElseIf(false, 2). Else(3) @@ -304,7 +304,7 @@ func ExampleSwitch() { // 3 } -func ExampleswitchCase_Case() { +func Example_switchCase_Case() { result1 := Switch[int, string](1). Case(1, "1"). Case(2, "2"). @@ -350,7 +350,7 @@ func ExampleswitchCase_Case() { // 3 } -func ExampleswitchCase_CaseF() { +func Example_switchCase_CaseF() { result1 := Switch[int, string](1). Case(1, "1"). Case(2, "2"). @@ -396,7 +396,7 @@ func ExampleswitchCase_CaseF() { // 3 } -func ExampleswitchCase_Default() { +func Example_switchCase_Default() { result1 := Switch[int, string](1). Case(1, "1"). Case(2, "2"). @@ -442,7 +442,7 @@ func ExampleswitchCase_Default() { // 3 } -func ExampleswitchCase_DefaultF() { +func Example_switchCase_DefaultF() { result1 := Switch[int, string](1). Case(1, "1"). Case(2, "2"). diff --git a/errors.go b/errors.go index 810c2e72..493580b1 100644 --- a/errors.go +++ b/errors.go @@ -10,12 +10,12 @@ import ( // Play: https://go.dev/play/p/vPyh51XpCBt func Validate(ok bool, format string, args ...any) error { if !ok { - return fmt.Errorf(fmt.Sprintf(format, args...)) + return fmt.Errorf(format, args...) } return nil } -func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { +func messageFromMsgAndArgs(msgAndArgs ...any) string { if len(msgAndArgs) == 1 { if msgAsStr, ok := msgAndArgs[0].(string); ok { return msgAsStr @@ -29,7 +29,7 @@ func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { } // must panics if err is error or false. -func must(err any, messageArgs ...interface{}) { +func must(err any, messageArgs ...any) { if err == nil { return } @@ -61,54 +61,54 @@ func must(err any, messageArgs ...interface{}) { // Must is a helper that wraps a call to a function returning a value and an error // and panics if err is error or false. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must[T any](val T, err any, messageArgs ...interface{}) T { +func Must[T any](val T, err any, messageArgs ...any) T { must(err, messageArgs...) return val } // Must0 has the same behavior as Must, but callback returns no variable. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must0(err any, messageArgs ...interface{}) { +func Must0(err any, messageArgs ...any) { must(err, messageArgs...) } // Must1 is an alias to Must // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must1[T any](val T, err any, messageArgs ...interface{}) T { +func Must1[T any](val T, err any, messageArgs ...any) T { return Must(val, err, messageArgs...) } // Must2 has the same behavior as Must, but callback returns 2 variables. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...interface{}) (T1, T2) { +func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...any) (T1, T2) { must(err, messageArgs...) return val1, val2 } // Must3 has the same behavior as Must, but callback returns 3 variables. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...interface{}) (T1, T2, T3) { +func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...any) (T1, T2, T3) { must(err, messageArgs...) return val1, val2, val3 } // Must4 has the same behavior as Must, but callback returns 4 variables. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...interface{}) (T1, T2, T3, T4) { +func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...any) (T1, T2, T3, T4) { must(err, messageArgs...) return val1, val2, val3, val4 } // Must5 has the same behavior as Must, but callback returns 5 variables. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5) { +func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...any) (T1, T2, T3, T4, T5) { must(err, messageArgs...) return val1, val2, val3, val4, val5 } // Must6 has the same behavior as Must, but callback returns 6 variables. // Play: https://go.dev/play/p/TMoWrRp3DyC -func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...interface{}) (T1, T2, T3, T4, T5, T6) { +func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...any) (T1, T2, T3, T4, T5, T6) { must(err, messageArgs...) return val1, val2, val3, val4, val5, val6 } diff --git a/errors_example_test.go b/errors_example_test.go index 1594a0c2..88006bd6 100644 --- a/errors_example_test.go +++ b/errors_example_test.go @@ -363,6 +363,7 @@ func ExampleTryOr5() { fmt.Printf("%v %v %v %v %v %v\n", value1, value2, value3, value4, value5, ok3) // Output: 21 hello false {bar} 4.2 false } + func ExampleTryOr6() { value1, value2, value3, value4, value5, value6, ok3 := TryOr6(func() (int, string, bool, foo, float64, string, error) { panic("my error") @@ -405,8 +406,7 @@ func ExampleTryCatchWithErrorValue() { // Output: catch: trigger an error } -type myError struct { -} +type myError struct{} func (e myError) Error() string { return "my error" diff --git a/errors_test.go b/errors_test.go index 08cace9d..11743a1f 100644 --- a/errors_test.go +++ b/errors_test.go @@ -53,7 +53,7 @@ func TestMust(t *testing.T) { is.PanicsWithValue("operation should fail: assert.AnError general error for testing", func() { Must0(cb(), "operation should fail") }) - + is.PanicsWithValue("must: invalid err type 'int', should either be a bool or an error", func() { Must0(0) }) @@ -271,11 +271,11 @@ func TestTry(t *testing.T) { func TestTryX(t *testing.T) { t.Parallel() is := assert.New(t) - + is.True(Try1(func() error { return nil })) - + is.True(Try2(func() (string, error) { return "", nil })) @@ -295,11 +295,11 @@ func TestTryX(t *testing.T) { is.True(Try6(func() (string, string, string, string, string, error) { return "", "", "", "", "", nil })) - + is.False(Try1(func() error { panic("error") })) - + is.False(Try2(func() (string, error) { panic("error") })) @@ -319,11 +319,11 @@ func TestTryX(t *testing.T) { is.False(Try6(func() (string, string, string, string, string, error) { panic("error") })) - + is.False(Try1(func() error { return errors.New("foo") })) - + is.False(Try2(func() (string, error) { return "", errors.New("foo") })) @@ -513,13 +513,13 @@ func TestTryWithErrorValue(t *testing.T) { }) is.False(ok) is.Equal("error", err) - + err, ok = TryWithErrorValue(func() error { return errors.New("foo") }) is.False(ok) is.EqualError(err.(error), "foo") - + err, ok = TryWithErrorValue(func() error { return nil }) @@ -535,7 +535,7 @@ func TestTryCatch(t *testing.T) { TryCatch(func() error { panic("error") }, func() { - //error was caught + // error was caught caught = true }) is.True(caught) @@ -544,7 +544,7 @@ func TestTryCatch(t *testing.T) { TryCatch(func() error { return nil }, func() { - //no error to be caught + // no error to be caught caught = true }) is.False(caught) @@ -558,7 +558,7 @@ func TestTryCatchWithErrorValue(t *testing.T) { TryCatchWithErrorValue(func() error { panic("error") }, func(val any) { - //error was caught + // error was caught caught = val == "error" }) is.True(caught) @@ -567,7 +567,7 @@ func TestTryCatchWithErrorValue(t *testing.T) { TryCatchWithErrorValue(func() error { return nil }, func(val any) { - //no error to be caught + // no error to be caught caught = true }) is.False(caught) diff --git a/find.go b/find.go index 93329a0b..59c23460 100644 --- a/find.go +++ b/find.go @@ -2,14 +2,12 @@ package lo import ( "fmt" - "math/rand" "time" - "golang.org/x/exp/constraints" + "github.com/samber/lo/internal/constraints" + "github.com/samber/lo/internal/rand" ) -// import "golang.org/x/exp/constraints" - // IndexOf returns the index at which the first occurrence of a value is found in an array or return -1 // if the value cannot be found. func IndexOf[T comparable](collection []T, element T) int { @@ -111,7 +109,7 @@ func FindKeyBy[K comparable, V any](object map[K]V, predicate func(key K, value // FindUniques returns a slice with all the unique elements of the collection. // The order of result values is determined by the order they occur in the collection. -func FindUniques[T comparable](collection []T) []T { +func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice { isDupl := make(map[T]bool, len(collection)) for i := range collection { @@ -123,7 +121,7 @@ func FindUniques[T comparable](collection []T) []T { } } - result := make([]T, 0, len(collection)-len(isDupl)) + result := make(Slice, 0, len(collection)-len(isDupl)) for i := range collection { if duplicated := isDupl[collection[i]]; !duplicated { @@ -137,7 +135,7 @@ func FindUniques[T comparable](collection []T) []T { // FindUniquesBy returns a slice with all the unique elements of the collection. // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. -func FindUniquesBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { +func FindUniquesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { isDupl := make(map[U]bool, len(collection)) for i := range collection { @@ -151,7 +149,7 @@ func FindUniquesBy[T any, U comparable](collection []T, iteratee func(item T) U) } } - result := make([]T, 0, len(collection)-len(isDupl)) + result := make(Slice, 0, len(collection)-len(isDupl)) for i := range collection { key := iteratee(collection[i]) @@ -166,7 +164,7 @@ func FindUniquesBy[T any, U comparable](collection []T, iteratee func(item T) U) // FindDuplicates returns a slice with the first occurrence of each duplicated elements of the collection. // The order of result values is determined by the order they occur in the collection. -func FindDuplicates[T comparable](collection []T) []T { +func FindDuplicates[T comparable, Slice ~[]T](collection Slice) Slice { isDupl := make(map[T]bool, len(collection)) for i := range collection { @@ -178,7 +176,7 @@ func FindDuplicates[T comparable](collection []T) []T { } } - result := make([]T, 0, len(collection)-len(isDupl)) + result := make(Slice, 0, len(collection)-len(isDupl)) for i := range collection { if duplicated := isDupl[collection[i]]; duplicated { @@ -193,7 +191,7 @@ func FindDuplicates[T comparable](collection []T) []T { // FindDuplicatesBy returns a slice with the first occurrence of each duplicated elements of the collection. // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. -func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { +func FindDuplicatesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { isDupl := make(map[U]bool, len(collection)) for i := range collection { @@ -207,7 +205,7 @@ func FindDuplicatesBy[T any, U comparable](collection []T, iteratee func(item T) } } - result := make([]T, 0, len(collection)-len(isDupl)) + result := make(Slice, 0, len(collection)-len(isDupl)) for i := range collection { key := iteratee(collection[i]) @@ -288,6 +286,30 @@ func Earliest(times ...time.Time) time.Time { return min } +// EarliestBy search the minimum time.Time of a collection using the given iteratee function. +// Returns zero value when the collection is empty. +func EarliestBy[T any](collection []T, iteratee func(item T) time.Time) T { + var earliest T + + if len(collection) == 0 { + return earliest + } + + earliest = collection[0] + earliestTime := iteratee(collection[0]) + + for i := 1; i < len(collection); i++ { + itemTime := iteratee(collection[i]) + + if itemTime.Before(earliestTime) { + earliest = collection[i] + earliestTime = itemTime + } + } + + return earliest +} + // Max searches the maximum value of a collection. // Returns zero value when the collection is empty. func Max[T constraints.Ordered](collection []T) T { @@ -355,6 +377,30 @@ func Latest(times ...time.Time) time.Time { return max } +// LatestBy search the maximum time.Time of a collection using the given iteratee function. +// Returns zero value when the collection is empty. +func LatestBy[T any](collection []T, iteratee func(item T) time.Time) T { + var latest T + + if len(collection) == 0 { + return latest + } + + latest = collection[0] + latestTime := iteratee(collection[0]) + + for i := 1; i < len(collection); i++ { + itemTime := iteratee(collection[i]) + + if itemTime.After(latestTime) { + latest = collection[i] + latestTime = itemTime + } + } + + return latest +} + // First returns the first element of a collection and check for availability of the first element. func First[T any](collection []T) (T, bool) { length := len(collection) @@ -395,7 +441,7 @@ func Last[T any](collection []T) (T, bool) { return collection[length-1], true } -// Returns the last element of a collection or zero value if empty. +// LastOrEmpty returns the last element of a collection or zero value if empty. func LastOrEmpty[T any](collection []T) T { i, _ := Last(collection) return i @@ -434,25 +480,21 @@ func Sample[T any](collection []T) T { return Empty[T]() } - // @TODO: Upgrade to math/rand/v2 as soon as we set the minimum Go version to 1.22. - // bearer:disable go_gosec_crypto_weak_random - return collection[rand.Intn(size)] + return collection[rand.IntN(size)] } // Samples returns N random unique items from collection. -func Samples[T any](collection []T, count int) []T { +func Samples[T any, Slice ~[]T](collection Slice, count int) Slice { size := len(collection) - copy := append([]T{}, collection...) + copy := append(Slice{}, collection...) - results := []T{} + results := Slice{} for i := 0; i < size && i < count; i++ { copyLength := size - i - // @TODO: Upgrade to math/rand/v2 as soon as we set the minimum Go version to 1.22. - // bearer:disable go_gosec_crypto_weak_random - index := rand.Intn(size - i) + index := rand.IntN(size - i) results = append(results, copy[index]) // Removes element. diff --git a/find_test.go b/find_test.go index 907004ec..b1533997 100644 --- a/find_test.go +++ b/find_test.go @@ -184,6 +184,11 @@ func TestFindUniques(t *testing.T) { is.Equal(0, len(result4)) is.Equal([]int{}, result4) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := FindUniques(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestFindUniquesBy(t *testing.T) { @@ -217,6 +222,13 @@ func TestFindUniquesBy(t *testing.T) { is.Equal(0, len(result4)) is.Equal([]int{}, result4) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := FindUniquesBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestFindDuplicates(t *testing.T) { @@ -237,6 +249,11 @@ func TestFindDuplicates(t *testing.T) { is.Equal(0, len(result3)) is.Equal([]int{}, result3) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := FindDuplicates(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestFindDuplicatesBy(t *testing.T) { @@ -263,6 +280,13 @@ func TestFindDuplicatesBy(t *testing.T) { is.Equal(0, len(result3)) is.Equal([]int{}, result3) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := FindDuplicatesBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestMin(t *testing.T) { @@ -312,6 +336,32 @@ func TestEarliest(t *testing.T) { is.Equal(result2, time.Time{}) } +func TestEarliestBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type foo struct { + bar time.Time + } + + t1 := time.Now() + t2 := t1.Add(time.Hour) + t3 := t1.Add(-time.Hour) + result1 := EarliestBy([]foo{{t1}, {t2}, {t3}}, func(i foo) time.Time { + return i.bar + }) + result2 := EarliestBy([]foo{{t1}}, func(i foo) time.Time { + return i.bar + }) + result3 := EarliestBy([]foo{}, func(i foo) time.Time { + return i.bar + }) + + is.Equal(result1, foo{t3}) + is.Equal(result2, foo{t1}) + is.Equal(result3, foo{}) +} + func TestMax(t *testing.T) { t.Parallel() is := assert.New(t) @@ -359,6 +409,32 @@ func TestLatest(t *testing.T) { is.Equal(result2, time.Time{}) } +func TestLatestBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type foo struct { + bar time.Time + } + + t1 := time.Now() + t2 := t1.Add(time.Hour) + t3 := t1.Add(-time.Hour) + result1 := LatestBy([]foo{{t1}, {t2}, {t3}}, func(i foo) time.Time { + return i.bar + }) + result2 := LatestBy([]foo{{t1}}, func(i foo) time.Time { + return i.bar + }) + result3 := LatestBy([]foo{}, func(i foo) time.Time { + return i.bar + }) + + is.Equal(result1, foo{t2}) + is.Equal(result2, foo{t1}) + is.Equal(result3, foo{}) +} + func TestFirst(t *testing.T) { t.Parallel() is := assert.New(t) @@ -488,4 +564,9 @@ func TestSamples(t *testing.T) { is.Equal(result1, []string{"a", "b", "c"}) is.Equal(result2, []string{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Samples(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") } diff --git a/go.mod b/go.mod index 3dd65690..2b189744 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,13 @@ module github.com/samber/lo go 1.18 // -// Dependencies are excluded from releases. Please check CI. +// Dev dependencies are excluded from releases. Please check CI. // require ( github.com/stretchr/testify v1.8.0 github.com/thoas/go-funk v0.9.1 go.uber.org/goleak v1.2.1 - golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 golang.org/x/text v0.16.0 ) diff --git a/go.sum b/go.sum index 7490ad11..7a7ead69 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,6 @@ github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= -golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/constraints/constraints.go b/internal/constraints/constraints.go new file mode 100644 index 00000000..3eb1cda5 --- /dev/null +++ b/internal/constraints/constraints.go @@ -0,0 +1,42 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} diff --git a/internal/constraints/ordered_go118.go b/internal/constraints/ordered_go118.go new file mode 100644 index 00000000..a124366f --- /dev/null +++ b/internal/constraints/ordered_go118.go @@ -0,0 +1,11 @@ +//go:build !go1.21 + +package constraints + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type Ordered interface { + Integer | Float | ~string +} diff --git a/internal/constraints/ordered_go121.go b/internal/constraints/ordered_go121.go new file mode 100644 index 00000000..c02de935 --- /dev/null +++ b/internal/constraints/ordered_go121.go @@ -0,0 +1,9 @@ +//go:build go1.21 + +package constraints + +import ( + "cmp" +) + +type Ordered = cmp.Ordered diff --git a/internal/rand/ordered_go118.go b/internal/rand/ordered_go118.go new file mode 100644 index 00000000..9fbc5385 --- /dev/null +++ b/internal/rand/ordered_go118.go @@ -0,0 +1,26 @@ +//go:build !go1.22 + +package rand + +import "math/rand" + +func Shuffle(n int, swap func(i, j int)) { + rand.Shuffle(n, swap) +} + +func IntN(n int) int { + // bearer:disable go_gosec_crypto_weak_random + return rand.Intn(n) +} + +func Int64() int64 { + // bearer:disable go_gosec_crypto_weak_random + n := rand.Int63() + + // bearer:disable go_gosec_crypto_weak_random + if rand.Intn(2) == 0 { + return -n + } + + return n +} diff --git a/internal/rand/ordered_go122.go b/internal/rand/ordered_go122.go new file mode 100644 index 00000000..65abf51a --- /dev/null +++ b/internal/rand/ordered_go122.go @@ -0,0 +1,17 @@ +//go:build go1.22 + +package rand + +import "math/rand/v2" + +func Shuffle(n int, swap func(i, j int)) { + rand.Shuffle(n, swap) +} + +func IntN(n int) int { + return rand.IntN(n) +} + +func Int64() int64 { + return rand.Int64() +} diff --git a/intersect.go b/intersect.go index 4e239d85..b2cf0727 100644 --- a/intersect.go +++ b/intersect.go @@ -33,7 +33,7 @@ func Every[T comparable](collection []T, subset []T) bool { return true } -// EveryBy returns true if the predicate returns true for all of the elements in the collection or if the collection is empty. +// EveryBy returns true if the predicate returns true for all elements in the collection or if the collection is empty. func EveryBy[T any](collection []T, predicate func(item T) bool) bool { for i := range collection { if !predicate(collection[i]) { @@ -91,8 +91,8 @@ func NoneBy[T any](collection []T, predicate func(item T) bool) bool { } // Intersect returns the intersection between two collections. -func Intersect[T comparable](list1 []T, list2 []T) []T { - result := []T{} +func Intersect[T comparable, Slice ~[]T](list1 Slice, list2 Slice) Slice { + result := Slice{} seen := map[T]struct{}{} for i := range list1 { @@ -111,9 +111,9 @@ func Intersect[T comparable](list1 []T, list2 []T) []T { // Difference returns the difference between two collections. // The first value is the collection of element absent of list2. // The second value is the collection of element absent of list1. -func Difference[T comparable](list1 []T, list2 []T) ([]T, []T) { - left := []T{} - right := []T{} +func Difference[T comparable, Slice ~[]T](list1 Slice, list2 Slice) (Slice, Slice) { + left := Slice{} + right := Slice{} seenLeft := map[T]struct{}{} seenRight := map[T]struct{}{} @@ -143,9 +143,15 @@ func Difference[T comparable](list1 []T, list2 []T) ([]T, []T) { // Union returns all distinct elements from given collections. // result returns will not change the order of elements relatively. -func Union[T comparable](lists ...[]T) []T { - result := []T{} - seen := map[T]struct{}{} +func Union[T comparable, Slice ~[]T](lists ...Slice) Slice { + var capLen int + + for _, list := range lists { + capLen += len(list) + } + + result := make(Slice, 0, capLen) + seen := make(map[T]struct{}, capLen) for i := range lists { for j := range lists[i] { @@ -160,8 +166,8 @@ func Union[T comparable](lists ...[]T) []T { } // Without returns slice excluding all given values. -func Without[T comparable](collection []T, exclude ...T) []T { - result := make([]T, 0, len(collection)) +func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice { + result := make(Slice, 0, len(collection)) for i := range collection { if !Contains(exclude, collection[i]) { result = append(result, collection[i]) @@ -171,15 +177,8 @@ func Without[T comparable](collection []T, exclude ...T) []T { } // WithoutEmpty returns slice excluding empty values. -func WithoutEmpty[T comparable](collection []T) []T { - var empty T - - result := make([]T, 0, len(collection)) - for i := range collection { - if collection[i] != empty { - result = append(result, collection[i]) - } - } - - return result +// +// Deprecated: Use lo.Compact instead. +func WithoutEmpty[T comparable, Slice ~[]T](collection Slice) Slice { + return Compact(collection) } diff --git a/intersect_test.go b/intersect_test.go index 339dbcbb..53911599 100644 --- a/intersect_test.go +++ b/intersect_test.go @@ -187,6 +187,11 @@ func TestIntersect(t *testing.T) { is.Equal(result3, []int{}) is.Equal(result4, []int{0}) is.Equal(result5, []int{0}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Intersect(allStrings, allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestDifference(t *testing.T) { @@ -204,6 +209,12 @@ func TestDifference(t *testing.T) { left3, right3 := Difference([]int{0, 1, 2, 3, 4, 5}, []int{0, 1, 2, 3, 4, 5}) is.Equal(left3, []int{}) is.Equal(right3, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + a, b := Difference(allStrings, allStrings) + is.IsType(a, allStrings, "type preserved") + is.IsType(b, allStrings, "type preserved") } func TestUnion(t *testing.T) { @@ -231,6 +242,11 @@ func TestUnion(t *testing.T) { is.Equal(result13, []int{0, 1, 2, 3, 4, 5}) is.Equal(result14, []int{0, 1, 2}) is.Equal(result15, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Union(allStrings, allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestWithout(t *testing.T) { @@ -247,6 +263,11 @@ func TestWithout(t *testing.T) { is.Equal(result3, []int{}) is.Equal(result4, []int{}) is.Equal(result5, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Without(allStrings, "") + is.IsType(nonempty, allStrings, "type preserved") } func TestWithoutEmpty(t *testing.T) { @@ -259,4 +280,9 @@ func TestWithoutEmpty(t *testing.T) { is.Equal(result1, []int{1, 2}) is.Equal(result2, []int{1, 2}) is.Equal(result3, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := WithoutEmpty(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } diff --git a/lo_test.go b/lo_test.go index 26db4c25..a3083dfa 100644 --- a/lo_test.go +++ b/lo_test.go @@ -1,7 +1,6 @@ package lo import ( - "os" "testing" "time" ) @@ -13,12 +12,12 @@ func testWithTimeout(t *testing.T, timeout time.Duration) { testFinished := make(chan struct{}) t.Cleanup(func() { close(testFinished) }) - go func() { + go func() { //nolint:staticcheck select { case <-testFinished: case <-time.After(timeout): t.Errorf("test timed out after %s", timeout) - os.Exit(1) + t.FailNow() //nolint:govet,staticcheck } }() } diff --git a/map.go b/map.go index b121daa3..d8feb434 100644 --- a/map.go +++ b/map.go @@ -2,23 +2,91 @@ package lo // Keys creates an array of the map keys. // Play: https://go.dev/play/p/Uu11fHASqrU -func Keys[K comparable, V any](in map[K]V) []K { - result := make([]K, 0, len(in)) +func Keys[K comparable, V any](in ...map[K]V) []K { + size := 0 + for i := range in { + size += len(in[i]) + } + result := make([]K, 0, size) - for k := range in { - result = append(result, k) + for i := range in { + for k := range in[i] { + result = append(result, k) + } + } + + return result +} + +// UniqKeys creates an array of unique keys in the map. +// Play: https://go.dev/play/p/TPKAb6ILdHk +func UniqKeys[K comparable, V any](in ...map[K]V) []K { + size := 0 + for i := range in { + size += len(in[i]) + } + + seen := make(map[K]struct{}, size) + result := make([]K, 0) + + for i := range in { + for k := range in[i] { + if _, exists := seen[k]; exists { + continue + } + seen[k] = struct{}{} + result = append(result, k) + } } return result } +// HasKey returns whether the given key exists. +// Play: https://go.dev/play/p/aVwubIvECqS +func HasKey[K comparable, V any](in map[K]V, key K) bool { + _, ok := in[key] + return ok +} + // Values creates an array of the map values. // Play: https://go.dev/play/p/nnRTQkzQfF6 -func Values[K comparable, V any](in map[K]V) []V { - result := make([]V, 0, len(in)) +func Values[K comparable, V any](in ...map[K]V) []V { + size := 0 + for i := range in { + size += len(in[i]) + } + result := make([]V, 0, size) - for k := range in { - result = append(result, in[k]) + for i := range in { + for k := range in[i] { + result = append(result, in[i][k]) + } + } + + return result +} + +// UniqValues creates an array of unique values in the map. +// Play: https://go.dev/play/p/nf6bXMh7rM3 +func UniqValues[K comparable, V comparable](in ...map[K]V) []V { + size := 0 + for i := range in { + size += len(in[i]) + } + + seen := make(map[V]struct{}, size) + result := make([]V, 0) + + for i := range in { + for k := range in[i] { + val := in[i][k] + if _, exists := seen[val]; exists { + continue + } + seen[val] = struct{}{} + result = append(result, val) + } } return result @@ -35,8 +103,8 @@ func ValueOr[K comparable, V any](in map[K]V, key K, fallback V) V { // PickBy returns same map type filtered by given predicate. // Play: https://go.dev/play/p/kdg8GR_QMmf -func PickBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) map[K]V { - r := map[K]V{} +func PickBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) bool) Map { + r := Map{} for k := range in { if predicate(k, in[k]) { r[k] = in[k] @@ -47,8 +115,8 @@ func PickBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool // PickByKeys returns same map type filtered by given keys. // Play: https://go.dev/play/p/R1imbuci9qU -func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { - r := map[K]V{} +func PickByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map { + r := Map{} for i := range keys { if v, ok := in[keys[i]]; ok { r[keys[i]] = v @@ -59,8 +127,8 @@ func PickByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { // PickByValues returns same map type filtered by given values. // Play: https://go.dev/play/p/1zdzSvbfsJc -func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { - r := map[K]V{} +func PickByValues[K comparable, V comparable, Map ~map[K]V](in Map, values []V) Map { + r := Map{} for k := range in { if Contains(values, in[k]) { r[k] = in[k] @@ -71,8 +139,8 @@ func PickByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { // OmitBy returns same map type filtered by given predicate. // Play: https://go.dev/play/p/EtBsR43bdsd -func OmitBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) map[K]V { - r := map[K]V{} +func OmitBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) bool) Map { + r := Map{} for k := range in { if !predicate(k, in[k]) { r[k] = in[k] @@ -83,8 +151,8 @@ func OmitBy[K comparable, V any](in map[K]V, predicate func(key K, value V) bool // OmitByKeys returns same map type filtered by given keys. // Play: https://go.dev/play/p/t1QjCrs-ysk -func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { - r := map[K]V{} +func OmitByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map { + r := Map{} for k := range in { r[k] = in[k] } @@ -96,8 +164,8 @@ func OmitByKeys[K comparable, V any](in map[K]V, keys []K) map[K]V { // OmitByValues returns same map type filtered by given values. // Play: https://go.dev/play/p/9UYZi-hrs8j -func OmitByValues[K comparable, V comparable](in map[K]V, values []V) map[K]V { - r := map[K]V{} +func OmitByValues[K comparable, V comparable, Map ~map[K]V](in Map, values []V) Map { + r := Map{} for k := range in { if !Contains(values, in[k]) { r[k] = in[k] @@ -163,9 +231,13 @@ func Invert[K comparable, V comparable](in map[K]V) map[V]K { // Assign merges multiple maps from left to right. // Play: https://go.dev/play/p/VhwfJOyxf5o -func Assign[K comparable, V any](maps ...map[K]V) map[K]V { - out := map[K]V{} +func Assign[K comparable, V any, Map ~map[K]V](maps ...Map) Map { + count := 0 + for i := range maps { + count += len(maps[i]) + } + out := make(Map, count) for i := range maps { for k := range maps[i] { out[k] = maps[i][k] diff --git a/map_benchmark_test.go b/map_benchmark_test.go new file mode 100644 index 00000000..ccc2ded5 --- /dev/null +++ b/map_benchmark_test.go @@ -0,0 +1,124 @@ +package lo + +import ( + "math/rand" + "strconv" + "testing" + "time" + + lop "github.com/samber/lo/parallel" + "github.com/thoas/go-funk" +) + +func sliceGenerator(size uint) []int64 { + r := rand.New(rand.NewSource(time.Now().Unix())) + + result := make([]int64, size) + + for i := uint(0); i < size; i++ { + result[i] = r.Int63() + } + + return result +} + +func mapGenerator(size uint) map[int64]int64 { + r := rand.New(rand.NewSource(time.Now().Unix())) + + result := make(map[int64]int64, size) + + for i := uint(0); i < size; i++ { + result[int64(i)] = r.Int63() + } + + return result +} + +func BenchmarkMap(b *testing.B) { + arr := sliceGenerator(1000000) + + b.Run("lo.Map", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = Map(arr, func(x int64, i int) string { + return strconv.FormatInt(x, 10) + }) + } + }) + + b.Run("lop.Map", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = lop.Map(arr, func(x int64, i int) string { + return strconv.FormatInt(x, 10) + }) + } + }) + + b.Run("reflect", func(b *testing.B) { + for n := 0; n < b.N; n++ { + _ = funk.Map(arr, func(x int64) string { + return strconv.FormatInt(x, 10) + }) + } + }) + + b.Run("for", func(b *testing.B) { + for n := 0; n < b.N; n++ { + results := make([]string, len(arr)) + + for i, item := range arr { + result := strconv.FormatInt(item, 10) + results[i] = result + } + } + }) +} + +// also apply to UniqValues +func BenchmarkUniqKeys(b *testing.B) { + m := []map[int64]int64{ + mapGenerator(100000), + mapGenerator(100000), + mapGenerator(100000), + } + + // allocate just in time + ordered + b.Run("lo.UniqKeys.jit-alloc", func(b *testing.B) { + for n := 0; n < b.N; n++ { + seen := make(map[int64]struct{}) + result := make([]int64, 0) + + for i := range m { + for k := range m[i] { + if _, exists := seen[k]; exists { + continue + } + seen[k] = struct{}{} + result = append(result, k) //nolint:staticcheck + } + } + } + }) + + // preallocate + unordered + b.Run("lo.UniqKeys.preallocate", func(b *testing.B) { + for n := 0; n < b.N; n++ { + size := 0 + for i := range m { + size += len(m[i]) + } + seen := make(map[int64]struct{}, size) + + for i := range m { + for k := range m[i] { + seen[k] = struct{}{} + } + } + + result := make([]int64, 0, len(seen)) + + for k := range seen { + result = append(result, k) //nolint:staticcheck + } + } + }) +} diff --git a/map_example_test.go b/map_example_test.go index aebca8fd..e9347a04 100644 --- a/map_example_test.go +++ b/map_example_test.go @@ -9,20 +9,42 @@ import ( func ExampleKeys() { kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"baz": 3} - result := Keys(kv) + result := Keys(kv, kv2) + sort.Strings(result) + fmt.Printf("%v", result) + // Output: [bar baz foo] +} - sort.StringSlice(result).Sort() +func ExampleUniqKeys() { + kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"bar": 3} + + result := UniqKeys(kv, kv2) + sort.Strings(result) fmt.Printf("%v", result) // Output: [bar foo] } func ExampleValues() { kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"baz": 3} - result := Values(kv) + result := Values(kv, kv2) - sort.IntSlice(result).Sort() + sort.Ints(result) + fmt.Printf("%v", result) + // Output: [1 2 3] +} + +func ExampleUniqValues() { + kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"baz": 2} + + result := UniqValues(kv, kv2) + + sort.Ints(result) fmt.Printf("%v", result) // Output: [1 2] } @@ -149,8 +171,8 @@ func ExampleAssign() { func ExampleMapKeys() { kv := map[int]int{1: 1, 2: 2, 3: 3, 4: 4} - result := MapKeys(kv, func(_ int, v int) string { - return strconv.FormatInt(int64(v), 10) + result := MapKeys(kv, func(_ int, k int) string { + return strconv.FormatInt(int64(k), 10) }) fmt.Printf("%v %v %v %v %v", len(result), result["1"], result["2"], result["3"], result["4"]) @@ -160,12 +182,12 @@ func ExampleMapKeys() { func ExampleMapValues() { kv := map[int]int{1: 1, 2: 2, 3: 3, 4: 4} - result := MapValues(kv, func(_ int, v int) string { + result := MapValues(kv, func(v int, _ int) string { return strconv.FormatInt(int64(v), 10) }) - fmt.Printf("%v %v %v %v %v", len(result), result[1], result[2], result[3], result[4]) - // Output: 4 1 2 3 4 + fmt.Printf("%v %q %q %q %q", len(result), result[1], result[2], result[3], result[4]) + // Output: 4 "1" "2" "3" "4" } func ExampleMapEntries() { diff --git a/map_test.go b/map_test.go index c9154425..fb02bf9a 100644 --- a/map_test.go +++ b/map_test.go @@ -15,8 +15,59 @@ func TestKeys(t *testing.T) { r1 := Keys(map[string]int{"foo": 1, "bar": 2}) sort.Strings(r1) + is.Equal(r1, []string{"bar", "foo"}) + + r2 := Keys(map[string]int{}) + is.Empty(r2) + + r3 := Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) + sort.Strings(r3) + is.Equal(r3, []string{"bar", "baz", "foo"}) + + r4 := Keys[string, int]() + is.Equal(r4, []string{}) + + r5 := Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 3}) + sort.Strings(r5) + is.Equal(r5, []string{"bar", "bar", "foo"}) +} + +func TestUniqKeys(t *testing.T) { + t.Parallel() + is := assert.New(t) + r1 := UniqKeys(map[string]int{"foo": 1, "bar": 2}) + sort.Strings(r1) is.Equal(r1, []string{"bar", "foo"}) + + r2 := UniqKeys(map[string]int{}) + is.Empty(r2) + + r3 := UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) + sort.Strings(r3) + is.Equal(r3, []string{"bar", "baz", "foo"}) + + r4 := UniqKeys[string, int]() + is.Equal(r4, []string{}) + + r5 := UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3}) + sort.Strings(r5) + is.Equal(r5, []string{"bar", "foo"}) + + // check order + r6 := UniqKeys(map[string]int{"foo": 1}, map[string]int{"bar": 3}) + is.Equal(r6, []string{"foo", "bar"}) +} + +func TestHasKey(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := HasKey(map[string]int{"foo": 1}, "bar") + is.False(r1) + + r2 := HasKey(map[string]int{"foo": 1}, "foo") + is.True(r2) } func TestValues(t *testing.T) { @@ -25,8 +76,52 @@ func TestValues(t *testing.T) { r1 := Values(map[string]int{"foo": 1, "bar": 2}) sort.Ints(r1) + is.Equal(r1, []int{1, 2}) + + r2 := Values(map[string]int{}) + is.Empty(r2) + + r3 := Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) + sort.Ints(r3) + is.Equal(r3, []int{1, 2, 3}) + + r4 := Values[string, int]() + is.Equal(r4, []int{}) + + r5 := Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3}) + sort.Ints(r5) + is.Equal(r5, []int{1, 1, 2, 3}) +} +func TestUniqValues(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := UniqValues(map[string]int{"foo": 1, "bar": 2}) + sort.Ints(r1) is.Equal(r1, []int{1, 2}) + + r2 := UniqValues(map[string]int{}) + is.Empty(r2) + + r3 := UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3}) + sort.Ints(r3) + is.Equal(r3, []int{1, 2, 3}) + + r4 := UniqValues[string, int]() + is.Equal(r4, []int{}) + + r5 := UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3}) + sort.Ints(r5) + is.Equal(r5, []int{1, 2, 3}) + + r6 := UniqValues(map[string]int{"foo": 1, "bar": 1}, map[string]int{"foo": 1, "bar": 3}) + sort.Ints(r6) + is.Equal(r6, []int{1, 3}) + + // check order + r7 := UniqValues(map[string]int{"foo": 1}, map[string]int{"bar": 3}) + is.Equal(r7, []int{1, 3}) } func TestValueOr(t *testing.T) { @@ -49,6 +144,11 @@ func TestPickBy(t *testing.T) { }) is.Equal(r1, map[string]int{"foo": 1, "baz": 3}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := PickBy(before, func(key string, value int) bool { return true }) + is.IsType(after, before, "type preserved") } func TestPickByKeys(t *testing.T) { @@ -58,6 +158,11 @@ func TestPickByKeys(t *testing.T) { r1 := PickByKeys(map[string]int{"foo": 1, "bar": 2, "baz": 3}, []string{"foo", "baz", "qux"}) is.Equal(r1, map[string]int{"foo": 1, "baz": 3}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := PickByKeys(before, []string{"foobar", "baz"}) + is.IsType(after, before, "type preserved") } func TestPickByValues(t *testing.T) { @@ -67,6 +172,11 @@ func TestPickByValues(t *testing.T) { r1 := PickByValues(map[string]int{"foo": 1, "bar": 2, "baz": 3}, []int{1, 3}) is.Equal(r1, map[string]int{"foo": 1, "baz": 3}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := PickByValues(before, []int{0, 3}) + is.IsType(after, before, "type preserved") } func TestOmitBy(t *testing.T) { @@ -78,6 +188,11 @@ func TestOmitBy(t *testing.T) { }) is.Equal(r1, map[string]int{"bar": 2}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := PickBy(before, func(key string, value int) bool { return true }) + is.IsType(after, before, "type preserved") } func TestOmitByKeys(t *testing.T) { @@ -87,6 +202,11 @@ func TestOmitByKeys(t *testing.T) { r1 := OmitByKeys(map[string]int{"foo": 1, "bar": 2, "baz": 3}, []string{"foo", "baz", "qux"}) is.Equal(r1, map[string]int{"bar": 2}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := OmitByKeys(before, []string{"foobar", "baz"}) + is.IsType(after, before, "type preserved") } func TestOmitByValues(t *testing.T) { @@ -96,6 +216,11 @@ func TestOmitByValues(t *testing.T) { r1 := OmitByValues(map[string]int{"foo": 1, "bar": 2, "baz": 3}, []int{1, 3}) is.Equal(r1, map[string]int{"bar": 2}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := OmitByValues(before, []int{0, 3}) + is.IsType(after, before, "type preserved") } func TestEntries(t *testing.T) { @@ -200,6 +325,11 @@ func TestAssign(t *testing.T) { is.Len(result1, 3) is.Equal(result1, map[string]int{"a": 1, "b": 3, "c": 4}) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := Assign(before, before) + is.IsType(after, before, "type preserved") } func TestMapKeys(t *testing.T) { @@ -313,7 +443,8 @@ func TestMapEntries(t *testing.T) { }{"1-11-1": {name: "foo", age: 1}, "2-22-2": {name: "bar", age: 2}}, func(k string, v struct { name string age int - }) (string, string) { + }, + ) (string, string) { return v.name, k }, map[string]string{"bar": "2-22-2", "foo": "1-11-1"}) } @@ -335,3 +466,67 @@ func TestMapToSlice(t *testing.T) { is.ElementsMatch(result1, []string{"1_5", "2_6", "3_7", "4_8"}) is.ElementsMatch(result2, []string{"1", "2", "3", "4"}) } + +func BenchmarkAssign(b *testing.B) { + counts := []int{32768, 1024, 128, 32, 2} + + allDifferentMap := func(b *testing.B, n int) []map[string]int { + defer b.ResetTimer() + m := make([]map[string]int, 0) + for i := 0; i < n; i++ { + m = append(m, map[string]int{ + strconv.Itoa(i): i, + strconv.Itoa(i): i, + strconv.Itoa(i): i, + strconv.Itoa(i): i, + strconv.Itoa(i): i, + strconv.Itoa(i): i, + }, + ) + } + return m + } + + allTheSameMap := func(b *testing.B, n int) []map[string]int { + defer b.ResetTimer() + m := make([]map[string]int, 0) + for i := 0; i < n; i++ { + m = append(m, map[string]int{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + "f": 6, + }, + ) + } + return m + } + + for _, count := range counts { + differentMap := allDifferentMap(b, count) + sameMap := allTheSameMap(b, count) + + b.Run(fmt.Sprintf("%d", count), func(b *testing.B) { + testcase := []struct { + name string + maps []map[string]int + }{ + {"different", differentMap}, + {"same", sameMap}, + } + + for _, tc := range testcase { + b.Run(tc.name, func(b *testing.B) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + result := Assign(tc.maps...) + _ = result + } + }) + } + }) + + } +} diff --git a/math.go b/math.go index 3ed839f6..7a18ca62 100644 --- a/math.go +++ b/math.go @@ -1,6 +1,8 @@ package lo -import "golang.org/x/exp/constraints" +import ( + "github.com/samber/lo/internal/constraints" +) // Range creates an array of numbers (positive and/or negative) with given length. // Play: https://go.dev/play/p/0r6VimXAi9H @@ -85,20 +87,20 @@ func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Comple // Mean calculates the mean of a collection of numbers. func Mean[T constraints.Float | constraints.Integer](collection []T) T { - var length T = T(len(collection)) + var length = T(len(collection)) if length == 0 { return 0 } - var sum T = Sum(collection) + var sum = Sum(collection) return sum / length } // MeanBy calculates the mean of a collection of numbers using the given return value from the iteration function. func MeanBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(item T) R) R { - var length R = R(len(collection)) + var length = R(len(collection)) if length == 0 { return 0 } - var sum R = SumBy(collection, iteratee) + var sum = SumBy(collection, iteratee) return sum / length } diff --git a/parallel/slice.go b/parallel/slice.go index 20ba3abd..a70fb70f 100644 --- a/parallel/slice.go +++ b/parallel/slice.go @@ -67,8 +67,8 @@ func Times[T any](count int, iteratee func(index int) T) []T { // 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(item T) U) map[U][]T { - result := map[U][]T{} +func GroupBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) map[U]Slice { + result := map[U]Slice{} var mu sync.Mutex var wg sync.WaitGroup @@ -96,8 +96,8 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(item T) U) map[U // determined by the order they occur in collection. The grouping is generated from the results // of running each element of collection through iteratee. // `iteratee` is call in parallel. -func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [][]T { - result := [][]T{} +func PartitionBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) K) []Slice { + result := []Slice{} seen := map[K]int{} var mu sync.Mutex diff --git a/parallel/slice_test.go b/parallel/slice_test.go index 248b45da..786835b7 100644 --- a/parallel/slice_test.go +++ b/parallel/slice_test.go @@ -5,20 +5,20 @@ import ( "strconv" "sync/atomic" "testing" - + "github.com/stretchr/testify/assert" ) func TestMap(t *testing.T) { is := assert.New(t) - + result1 := Map([]int{1, 2, 3, 4}, func(x int, _ int) string { return "Hello" }) result2 := Map([]int64{1, 2, 3, 4}, func(x int64, _ int) string { return strconv.FormatInt(x, 10) }) - + is.Equal(len(result1), 4) is.Equal(len(result2), 4) is.Equal(result1, []string{"Hello", "Hello", "Hello", "Hello"}) @@ -27,52 +27,59 @@ func TestMap(t *testing.T) { func TestForEach(t *testing.T) { is := assert.New(t) - + var counter uint64 collection := []int{1, 2, 3, 4} ForEach(collection, func(x int, i int) { atomic.AddUint64(&counter, 1) }) - + is.Equal(uint64(4), atomic.LoadUint64(&counter)) } func TestTimes(t *testing.T) { is := assert.New(t) - + result1 := Times(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) - + result1 := GroupBy([]int{0, 1, 2, 3, 4, 5}, func(i int) int { return i % 3 }) - + // order for x := range result1 { sort.Slice(result1[x], func(i, j int) bool { return result1[x][i] < result1[x][j] }) } - + is.EqualValues(len(result1), 3) is.EqualValues(result1, map[int][]int{ 0: {0, 3}, 1: {1, 4}, 2: {2, 5}, }) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := GroupBy(allStrings, func(i string) int { + return 42 + }) + is.IsType(nonempty[42], allStrings, "type preserved") } func TestPartitionBy(t *testing.T) { is := assert.New(t) - + oddEven := func(x int) string { if x < 0 { return "negative" @@ -81,10 +88,10 @@ func TestPartitionBy(t *testing.T) { } return "odd" } - + result1 := PartitionBy([]int{-2, -1, 0, 1, 2, 3, 4, 5}, oddEven) result2 := PartitionBy([]int{}, oddEven) - + // order sort.Slice(result1, func(i, j int) bool { return result1[i][0] < result1[j][0] @@ -94,7 +101,14 @@ func TestPartitionBy(t *testing.T) { return result1[x][i] < result1[x][j] }) } - + is.ElementsMatch(result1, [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}}) is.Equal(result2, [][]int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := PartitionBy(allStrings, func(item string) int { + return len(item) + }) + is.IsType(nonempty[0], allStrings, "type preserved") } diff --git a/retry.go b/retry.go index f026aa33..82e8f82f 100644 --- a/retry.go +++ b/retry.go @@ -104,7 +104,6 @@ func (d *debounceBy[T]) reset(key T) { for i := range d.callbacks { d.callbacks[i](key, count) } - }) } @@ -141,7 +140,8 @@ func NewDebounceBy[T comparable](duration time.Duration, f ...func(key T, count }, 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 successful response is returned. +// Attempt invokes a function N times until it returns valid output. Returns either the caught error or nil. +// When the first argument is less than `1`, the function runs until a successful response is returned. // Play: https://go.dev/play/p/3ggJZ2ZKcMj func Attempt(maxIteration int, f func(index int) error) (int, error) { var err error @@ -158,8 +158,8 @@ func Attempt(maxIteration int, f func(index int) error) (int, error) { } // AttemptWithDelay invokes a function N times until it returns valid output, -// with a pause between each call. Returning either the caught error or nil. -// When first argument is less than `1`, the function runs until a successful +// with a pause between each call. Returns either the caught error or nil. +// When the first argument is less than `1`, the function runs until a successful // response is returned. // Play: https://go.dev/play/p/tVs6CygC7m1 func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, duration time.Duration) error) (int, time.Duration, error) { @@ -182,9 +182,9 @@ func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, d } // AttemptWhile invokes a function N times until it returns valid output. -// Returning either the caught error or nil, and along with a bool value to identify -// whether it needs invoke function continuously. It will terminate the invoke -// immediately if second bool value is returned with falsy value. When first +// Returns either the caught error or nil, along with a bool value to determine +// whether the function should be invoked again. It will terminate the invoke +// immediately if the second return value is false. When the first // argument is less than `1`, the function runs until a successful response is // returned. func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) { @@ -206,10 +206,10 @@ func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) { } // AttemptWhileWithDelay invokes a function N times until it returns valid output, -// with a pause between each call. Returning either the caught error or nil, and along -// with a bool value to identify whether it needs to invoke function continuously. -// It will terminate the invoke immediately if second bool value is returned with falsy -// value. When first argument is less than `1`, the function runs until a successful +// with a pause between each call. Returns either the caught error or nil, along +// with a bool value to determine whether the function should be invoked again. +// It will terminate the invoke immediately if the second return value is false. +// When the first argument is less than `1`, the function runs until a successful // response is returned. func AttemptWhileWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, bool)) (int, time.Duration, error) { var err error diff --git a/retry_test.go b/retry_test.go index 1ac00703..f4094a76 100644 --- a/retry_test.go +++ b/retry_test.go @@ -82,7 +82,7 @@ func TestAttemptWithDelay(t *testing.T) { }) is.Equal(iter1, 1) - is.Greater(dur1, 0*time.Millisecond) + is.GreaterOrEqual(dur1, 0*time.Millisecond) is.Less(dur1, 1*time.Millisecond) is.Equal(err1, nil) is.Equal(iter2, 6) @@ -187,7 +187,7 @@ func TestAttemptWhileWithDelay(t *testing.T) { }) is.Equal(iter1, 1) - is.Greater(dur1, 0*time.Millisecond) + is.GreaterOrEqual(dur1, 0*time.Millisecond) is.Less(dur1, 1*time.Millisecond) is.Nil(err1) diff --git a/slice.go b/slice.go index 7ef820ad..d2d3fd84 100644 --- a/slice.go +++ b/slice.go @@ -1,15 +1,16 @@ package lo import ( - "math/rand" + "sort" - "golang.org/x/exp/constraints" + "github.com/samber/lo/internal/constraints" + "github.com/samber/lo/internal/rand" ) // Filter iterates over elements of collection, returning an array of all elements predicate returns truthy for. // Play: https://go.dev/play/p/Apjg3WeSi7K -func Filter[V any](collection []V, predicate func(item V, index int) bool) []V { - result := make([]V, 0, len(collection)) +func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) bool) Slice { + result := make(Slice, 0, len(collection)) for i := range collection { if predicate(collection[i], i) { @@ -93,6 +94,17 @@ func ForEach[T any](collection []T, iteratee func(item T, index int)) { } } +// ForEachWhile iterates over elements of collection and invokes iteratee for each element +// collection return value decide to continue or break, like do while(). +// Play: https://go.dev/play/p/QnLGt35tnow +func ForEachWhile[T any](collection []T, iteratee func(item T, index int) (goon bool)) { + for i := range collection { + if !iteratee(collection[i], i) { + break + } + } +} + // Times invokes the iteratee n times, returning an array of the results of each invocation. // The iteratee is invoked with index as argument. // Play: https://go.dev/play/p/vgQj3Glr6lT @@ -109,8 +121,8 @@ func Times[T any](count int, iteratee func(index int) T) []T { // 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. // Play: https://go.dev/play/p/DTzbeXZ6iEN -func Uniq[T comparable](collection []T) []T { - result := make([]T, 0, len(collection)) +func Uniq[T comparable, Slice ~[]T](collection Slice) Slice { + result := make(Slice, 0, len(collection)) seen := make(map[T]struct{}, len(collection)) for i := range collection { @@ -129,8 +141,8 @@ func Uniq[T comparable](collection []T) []T { // The order of result values is determined by the order they occur in the array. It accepts `iteratee` which is // invoked for each element in array to generate the criterion by which uniqueness is computed. // Play: https://go.dev/play/p/g42Z3QSb53u -func UniqBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { - result := make([]T, 0, len(collection)) +func UniqBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice { + result := make(Slice, 0, len(collection)) seen := make(map[U]struct{}, len(collection)) for i := range collection { @@ -149,8 +161,8 @@ func UniqBy[T any, U comparable](collection []T, iteratee func(item T) U) []T { // GroupBy returns an object composed of keys generated from the results of running each element of collection through iteratee. // Play: https://go.dev/play/p/XnQBd_v6brd -func GroupBy[T any, U comparable](collection []T, iteratee func(item T) U) map[U][]T { - result := map[U][]T{} +func GroupBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) map[U]Slice { + result := map[U]Slice{} for i := range collection { key := iteratee(collection[i]) @@ -164,7 +176,7 @@ func GroupBy[T any, U comparable](collection []T, iteratee func(item T) U) map[U // Chunk returns an array of elements split into groups the length of size. If array can't be split evenly, // the final chunk will be the remaining elements. // Play: https://go.dev/play/p/EeKl0AuTehH -func Chunk[T any](collection []T, size int) [][]T { +func Chunk[T any, Slice ~[]T](collection Slice, size int) []Slice { if size <= 0 { panic("Second parameter must be greater than 0") } @@ -174,14 +186,14 @@ func Chunk[T any](collection []T, size int) [][]T { chunksNum += 1 } - result := make([][]T, 0, chunksNum) + result := make([]Slice, 0, chunksNum) for i := 0; i < chunksNum; i++ { last := (i + 1) * size if last > len(collection) { last = len(collection) } - result = append(result, collection[i*size:last]) + result = append(result, collection[i*size:last:last]) } return result @@ -191,8 +203,8 @@ func Chunk[T any](collection []T, size int) [][]T { // determined by the order they occur in collection. The grouping is generated from the results // of running each element of collection through iteratee. // Play: https://go.dev/play/p/NfQ_nGjkgXW -func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [][]T { - result := [][]T{} +func PartitionBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) K) []Slice { + result := []Slice{} seen := map[K]int{} for i := range collection { @@ -202,7 +214,7 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [ if !ok { resultIndex = len(result) seen[key] = resultIndex - result = append(result, []T{}) + result = append(result, Slice{}) } result[resultIndex] = append(result[resultIndex], collection[i]) @@ -217,13 +229,13 @@ func PartitionBy[T any, K comparable](collection []T, iteratee func(item T) K) [ // Flatten returns an array a single level deep. // Play: https://go.dev/play/p/rbp9ORaMpjw -func Flatten[T any](collection [][]T) []T { +func Flatten[T any, Slice ~[]T](collection []Slice) Slice { totalLen := 0 for i := range collection { totalLen += len(collection[i]) } - result := make([]T, 0, totalLen) + result := make(Slice, 0, totalLen) for i := range collection { result = append(result, collection[i]...) } @@ -233,9 +245,9 @@ func Flatten[T any](collection [][]T) []T { // Interleave round-robin alternating input slices and sequentially appending value at index into result // Play: https://go.dev/play/p/-RJkTLQEDVt -func Interleave[T any](collections ...[]T) []T { +func Interleave[T any, Slice ~[]T](collections ...Slice) Slice { if len(collections) == 0 { - return []T{} + return Slice{} } maxSize := 0 @@ -249,10 +261,10 @@ func Interleave[T any](collections ...[]T) []T { } if maxSize == 0 { - return []T{} + return Slice{} } - result := make([]T, totalSize) + result := make(Slice, totalSize) resultIdx := 0 for i := 0; i < maxSize; i++ { @@ -271,7 +283,7 @@ func Interleave[T any](collections ...[]T) []T { // Shuffle returns an array of shuffled values. Uses the Fisher-Yates shuffle algorithm. // Play: https://go.dev/play/p/Qp73bnTDnc7 -func Shuffle[T any](collection []T) []T { +func Shuffle[T any, Slice ~[]T](collection Slice) Slice { rand.Shuffle(len(collection), func(i, j int) { collection[i], collection[j] = collection[j], collection[i] }) @@ -281,7 +293,7 @@ func Shuffle[T any](collection []T) []T { // Reverse reverses array so that the first element becomes the last, the second element becomes the second to last, and so on. // Play: https://go.dev/play/p/fhUMLvZ7vS6 -func Reverse[T any](collection []T) []T { +func Reverse[T any, Slice ~[]T](collection Slice) Slice { length := len(collection) half := length / 2 @@ -368,30 +380,30 @@ func SliceToMap[T any, K comparable, V any](collection []T, transform func(item // Drop drops n elements from the beginning of a slice or array. // Play: https://go.dev/play/p/JswS7vXRJP2 -func Drop[T any](collection []T, n int) []T { +func Drop[T any, Slice ~[]T](collection Slice, n int) Slice { if len(collection) <= n { - return make([]T, 0) + return make(Slice, 0) } - result := make([]T, 0, len(collection)-n) + result := make(Slice, 0, len(collection)-n) return append(result, collection[n:]...) } // DropRight drops n elements from the end of a slice or array. // Play: https://go.dev/play/p/GG0nXkSJJa3 -func DropRight[T any](collection []T, n int) []T { +func DropRight[T any, Slice ~[]T](collection Slice, n int) Slice { if len(collection) <= n { - return []T{} + return Slice{} } - result := make([]T, 0, len(collection)-n) + result := make(Slice, 0, len(collection)-n) return append(result, collection[:len(collection)-n]...) } // DropWhile drops elements from the beginning of a slice or array while the predicate returns true. // Play: https://go.dev/play/p/7gBPYw2IK16 -func DropWhile[T any](collection []T, predicate func(item T) bool) []T { +func DropWhile[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Slice { i := 0 for ; i < len(collection); i++ { if !predicate(collection[i]) { @@ -399,13 +411,13 @@ func DropWhile[T any](collection []T, predicate func(item T) bool) []T { } } - result := make([]T, 0, len(collection)-i) + result := make(Slice, 0, len(collection)-i) return append(result, collection[i:]...) } // DropRightWhile drops elements from the end of a slice or array while the predicate returns true. // Play: https://go.dev/play/p/3-n71oEC0Hz -func DropRightWhile[T any](collection []T, predicate func(item T) bool) []T { +func DropRightWhile[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Slice { i := len(collection) - 1 for ; i >= 0; i-- { if !predicate(collection[i]) { @@ -413,14 +425,46 @@ func DropRightWhile[T any](collection []T, predicate func(item T) bool) []T { } } - result := make([]T, 0, i+1) + result := make(Slice, 0, i+1) return append(result, collection[:i+1]...) } +// DropByIndex drops elements from a slice or array by the index. +// A negative index will drop elements from the end of the slice. +// Play: https://go.dev/play/p/bPIH4npZRxS +func DropByIndex[T any](collection []T, indexes ...int) []T { + initialSize := len(collection) + if initialSize == 0 { + return make([]T, 0) + } + + for i := range indexes { + if indexes[i] < 0 { + indexes[i] = initialSize + indexes[i] + } + } + + indexes = Uniq(indexes) + sort.Ints(indexes) + + result := make([]T, 0, initialSize) + result = append(result, collection...) + + for i := range indexes { + if indexes[i]-i < 0 || indexes[i]-i >= initialSize-i { + continue + } + + result = append(result[:indexes[i]-i], result[indexes[i]-i+1:]...) + } + + return result +} + // Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return truthy for. // Play: https://go.dev/play/p/YkLMODy1WEL -func Reject[V any](collection []V, predicate func(item V, index int) bool) []V { - result := []V{} +func Reject[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) bool) Slice { + result := Slice{} for i := range collection { if !predicate(collection[i], i) { @@ -449,9 +493,9 @@ func RejectMap[T any, R any](collection []T, callback func(item T, index int) (R // FilterReject mixes Filter and Reject, this method returns two slices, one for the elements of collection that // predicate returns truthy for and one for the elements that predicate does not return truthy for. -func FilterReject[V any](collection []V, predicate func(V, int) bool) (kept []V, rejected []V) { - kept = make([]V, 0, len(collection)) - rejected = make([]V, 0, len(collection)) +func FilterReject[T any, Slice ~[]T](collection Slice, predicate func(T, int) bool) (kept Slice, rejected Slice) { + kept = make(Slice, 0, len(collection)) + rejected = make(Slice, 0, len(collection)) for i := range collection { if predicate(collection[i], i) { @@ -515,7 +559,7 @@ func CountValuesBy[T any, U comparable](collection []T, mapper func(item T) U) m // Subset returns a copy of a slice from `offset` up to `length` elements. Like `slice[start:start+length]`, but does not panic on overflow. // Play: https://go.dev/play/p/tOQu1GhFcog -func Subset[T any](collection []T, offset int, length uint) []T { +func Subset[T any, Slice ~[]T](collection Slice, offset int, length uint) Slice { size := len(collection) if offset < 0 { @@ -526,7 +570,7 @@ func Subset[T any](collection []T, offset int, length uint) []T { } if offset > size { - return []T{} + return Slice{} } if length > uint(size)-uint(offset) { @@ -538,11 +582,11 @@ func Subset[T any](collection []T, offset int, length uint) []T { // Slice returns a copy of a slice from `start` up to, but not including `end`. Like `slice[start:end]`, but does not panic on overflow. // Play: https://go.dev/play/p/8XWYhfMMA1h -func Slice[T any](collection []T, start int, end int) []T { +func Slice[T any, Slice ~[]T](collection Slice, start int, end int) Slice { size := len(collection) if start >= end { - return []T{} + return Slice{} } if start > size { @@ -564,8 +608,8 @@ func Slice[T any](collection []T, start int, end int) []T { // Replace returns a copy of the slice with the first n non-overlapping instances of old replaced by new. // Play: https://go.dev/play/p/XfPzmf9gql6 -func Replace[T comparable](collection []T, old T, new T, n int) []T { - result := make([]T, len(collection)) +func Replace[T comparable, Slice ~[]T](collection Slice, old T, new T, n int) Slice { + result := make(Slice, len(collection)) copy(result, collection) for i := range result { @@ -580,16 +624,16 @@ func Replace[T comparable](collection []T, old T, new T, n int) []T { // ReplaceAll returns a copy of the slice with all non-overlapping instances of old replaced by new. // Play: https://go.dev/play/p/a9xZFUHfYcV -func ReplaceAll[T comparable](collection []T, old T, new T) []T { +func ReplaceAll[T comparable, Slice ~[]T](collection Slice, old T, new T) Slice { return Replace(collection, old, new, -1) } // Compact returns a slice of all non-zero elements. // Play: https://go.dev/play/p/tXiy-iK6PAc -func Compact[T comparable](collection []T) []T { +func Compact[T comparable, Slice ~[]T](collection Slice) Slice { var zero T - result := make([]T, 0, len(collection)) + result := make(Slice, 0, len(collection)) for i := range collection { if collection[i] != zero { @@ -625,3 +669,27 @@ func IsSortedByKey[T any, K constraints.Ordered](collection []T, iteratee func(i return true } + +// Splice inserts multiple elements at index i. A negative index counts back +// from the end of the slice. The helper is protected against overflow errors. +// Play: https://go.dev/play/p/G5_GhkeSUBA +func Splice[T any, Slice ~[]T](collection Slice, i int, elements ...T) Slice { + sizeCollection := len(collection) + sizeElements := len(elements) + output := make(Slice, 0, sizeCollection+sizeElements) // preallocate memory for the output slice + + if sizeElements == 0 { + return append(output, collection...) // simple copy + } else if i > sizeCollection { + // positive overflow + return append(append(output, collection...), elements...) + } else if i < -sizeCollection { + // negative overflow + return append(append(output, elements...), collection...) + } else if i < 0 { + // backward + i = sizeCollection + i + } + + return append(append(append(output, collection[:i]...), elements...), collection[i:]...) +} diff --git a/slice_benchmark_test.go b/slice_benchmark_test.go index 6b085116..7a1ff5d9 100644 --- a/slice_benchmark_test.go +++ b/slice_benchmark_test.go @@ -151,6 +151,26 @@ func BenchmarkDropRightWhile(b *testing.B) { } } +func BenchmarkDropByIndex(b *testing.B) { + for _, n := range lengths { + strs := genSliceString(n) + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = DropByIndex(strs, n/4) + } + }) + } + + for _, n := range lengths { + ints := genSliceInt(n) + b.Run(fmt.Sprintf("ints%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = DropByIndex(ints, n/4) + } + }) + } +} + func BenchmarkReplace(b *testing.B) { lengths := []int{1_000, 10_000, 100_000} for _, n := range lengths { diff --git a/slice_example_test.go b/slice_example_test.go index b5b49d2b..f6b1b65b 100644 --- a/slice_example_test.go +++ b/slice_example_test.go @@ -89,6 +89,22 @@ func ExampleForEach() { // 4 } +func ExampleForEachWhile() { + list := []int64{1, 2, -math.MaxInt, 4} + + ForEachWhile(list, func(x int64, _ int) bool { + if x < 0 { + return false + } + fmt.Println(x) + return true + }) + + // Output: + // 1 + // 2 +} + func ExampleTimes() { result := Times(3, func(i int) string { return strconv.FormatInt(int64(i), 10) @@ -296,6 +312,15 @@ func ExampleDropRightWhile() { // Output: [0 1 2] } +func ExampleDropByIndex() { + list := []int{0, 1, 2, 3, 4, 5} + + result := DropByIndex(list, 2) + + fmt.Printf("%v", result) + // Output: [0 1 3 4 5] +} + func ExampleReject() { list := []int{0, 1, 2, 3, 4, 5} diff --git a/slice_test.go b/slice_test.go index e7e717fd..9f923eea 100644 --- a/slice_test.go +++ b/slice_test.go @@ -18,14 +18,19 @@ func TestFilter(t *testing.T) { r1 := Filter([]int{1, 2, 3, 4}, func(x int, _ int) bool { return x%2 == 0 }) - is.Equal(r1, []int{2, 4}) r2 := Filter([]string{"", "foo", "", "bar", ""}, func(x string, _ int) bool { return len(x) > 0 }) - is.Equal(r2, []string{"foo", "bar"}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Filter(allStrings, func(x string, _ int) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestMap(t *testing.T) { @@ -125,6 +130,12 @@ func TestReduceRight(t *testing.T) { }, []int{}) is.Equal(result1, []int{4, 5, 2, 3, 0, 1}) + + type collection []int + result3 := ReduceRight(collection{1, 2, 3, 4}, func(agg int, item int, _ int) int { + return agg + item + }, 10) + is.Equal(result3, 20) } func TestForEach(t *testing.T) { @@ -146,6 +157,29 @@ func TestForEach(t *testing.T) { is.IsIncreasing(callParams2) } +func TestForEachWhile(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // check of callback is called for every element and in proper order + + var callParams1 []string + var callParams2 []int + + ForEachWhile([]string{"a", "b", "c"}, func(item string, i int) bool { + if item == "c" { + return false + } + callParams1 = append(callParams1, item) + callParams2 = append(callParams2, i) + return true + }) + + is.ElementsMatch([]string{"a", "b"}, callParams1) + is.ElementsMatch([]int{0, 1}, callParams2) + is.IsIncreasing(callParams2) +} + func TestUniq(t *testing.T) { t.Parallel() is := assert.New(t) @@ -154,6 +188,11 @@ func TestUniq(t *testing.T) { is.Equal(len(result1), 2) is.Equal(result1, []int{1, 2}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Uniq(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestUniqBy(t *testing.T) { @@ -166,6 +205,13 @@ func TestUniqBy(t *testing.T) { is.Equal(len(result1), 3) is.Equal(result1, []int{0, 1, 2}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := UniqBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestGroupBy(t *testing.T) { @@ -182,6 +228,13 @@ func TestGroupBy(t *testing.T) { 1: {1, 4}, 2: {2, 5}, }) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := GroupBy(allStrings, func(i string) int { + return 42 + }) + is.IsType(nonempty[42], allStrings, "type preserved") } func TestChunk(t *testing.T) { @@ -200,6 +253,17 @@ func TestChunk(t *testing.T) { is.PanicsWithValue("Second parameter must be greater than 0", func() { Chunk([]int{0}, 0) }) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Chunk(allStrings, 2) + is.IsType(nonempty[0], allStrings, "type preserved") + + // appending to a chunk should not affect original array + originalArray := []int{0, 1, 2, 3, 4, 5} + result5 := Chunk(originalArray, 2) + result5[0] = append(result5[0], 6) + is.Equal(originalArray, []int{0, 1, 2, 3, 4, 5}) } func TestPartitionBy(t *testing.T) { @@ -220,6 +284,13 @@ func TestPartitionBy(t *testing.T) { is.Equal(result1, [][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}}) is.Equal(result2, [][]int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := PartitionBy(allStrings, func(item string) int { + return len(item) + }) + is.IsType(nonempty[0], allStrings, "type preserved") } func TestFlatten(t *testing.T) { @@ -229,9 +300,16 @@ func TestFlatten(t *testing.T) { result1 := Flatten([][]int{{0, 1}, {2, 3, 4, 5}}) is.Equal(result1, []int{0, 1, 2, 3, 4, 5}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Flatten([]myStrings{allStrings}) + is.IsType(nonempty, allStrings, "type preserved") } func TestInterleave(t *testing.T) { + is := assert.New(t) + tests := []struct { name string collections [][]int @@ -275,6 +353,11 @@ func TestInterleave(t *testing.T) { } }) } + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Interleave(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestShuffle(t *testing.T) { @@ -286,6 +369,11 @@ func TestShuffle(t *testing.T) { is.NotEqual(result1, []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) is.Equal(result2, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Shuffle(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestReverse(t *testing.T) { @@ -299,6 +387,11 @@ func TestReverse(t *testing.T) { is.Equal(result1, []int{5, 4, 3, 2, 1, 0}) is.Equal(result2, []int{6, 5, 4, 3, 2, 1, 0}) is.Equal(result3, []int{}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Reverse(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestFill(t *testing.T) { @@ -431,6 +524,11 @@ func TestDrop(t *testing.T) { is.Equal([]int{4}, Drop([]int{0, 1, 2, 3, 4}, 4)) is.Equal([]int{}, Drop([]int{0, 1, 2, 3, 4}, 5)) is.Equal([]int{}, Drop([]int{0, 1, 2, 3, 4}, 6)) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Drop(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") } func TestDropRight(t *testing.T) { @@ -443,6 +541,11 @@ func TestDropRight(t *testing.T) { is.Equal([]int{0}, DropRight([]int{0, 1, 2, 3, 4}, 4)) is.Equal([]int{}, DropRight([]int{0, 1, 2, 3, 4}, 5)) is.Equal([]int{}, DropRight([]int{0, 1, 2, 3, 4}, 6)) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := DropRight(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") } func TestDropWhile(t *testing.T) { @@ -460,6 +563,13 @@ func TestDropWhile(t *testing.T) { is.Equal([]int{0, 1, 2, 3, 4, 5, 6}, DropWhile([]int{0, 1, 2, 3, 4, 5, 6}, func(t int) bool { return t == 10 })) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := DropWhile(allStrings, func(t string) bool { + return t != "foo" + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestDropRightWhile(t *testing.T) { @@ -481,6 +591,34 @@ func TestDropRightWhile(t *testing.T) { is.Equal([]int{}, DropRightWhile([]int{0, 1, 2, 3, 4, 5, 6}, func(t int) bool { return t != 10 })) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := DropRightWhile(allStrings, func(t string) bool { + return t != "foo" + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestDropByIndex(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Equal([]int{1, 2, 3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 0)) + is.Equal([]int{3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 0, 1, 2)) + is.Equal([]int{0, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, -4, -2, -3)) + is.Equal([]int{0, 2, 3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, -4, -4)) + is.Equal([]int{2, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 3, 1, 0)) + is.Equal([]int{0, 1, 3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 2)) + is.Equal([]int{0, 1, 2, 3}, DropByIndex([]int{0, 1, 2, 3, 4}, 4)) + is.Equal([]int{0, 1, 2, 3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 5)) + is.Equal([]int{0, 1, 2, 3, 4}, DropByIndex([]int{0, 1, 2, 3, 4}, 100)) + is.Equal([]int{0, 1, 2, 3}, DropByIndex([]int{0, 1, 2, 3, 4}, -1)) + is.Equal([]int{}, DropByIndex([]int{}, 0, 1)) + is.Equal([]int{}, DropByIndex([]int{42}, 0, 1)) + is.Equal([]int{}, DropByIndex([]int{42}, 1, 0)) + is.Equal([]int{}, DropByIndex([]int{}, 1)) + is.Equal([]int{}, DropByIndex([]int{1}, 0)) } func TestReject(t *testing.T) { @@ -498,6 +636,13 @@ func TestReject(t *testing.T) { }) is.Equal(r2, []string{"foo", "bar"}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Reject(allStrings, func(x string, _ int) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") } func TestRejectMap(t *testing.T) { @@ -540,6 +685,14 @@ func TestFilterReject(t *testing.T) { is.Equal(left2, []string{"Smith", "Domin", "Olivia"}) is.Equal(right2, []string{"foo", "bar"}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + a, b := FilterReject(allStrings, func(x string, _ int) bool { + return len(x) > 0 + }) + is.IsType(a, allStrings, "type preserved") + is.IsType(b, allStrings, "type preserved") } func TestCount(t *testing.T) { @@ -642,6 +795,11 @@ func TestSubset(t *testing.T) { is.Equal([]int{3, 4}, out10) is.Equal([]int{1}, out11) is.Equal([]int{1, 2, 3, 4}, out12) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Subset(allStrings, 0, 2) + is.IsType(nonempty, allStrings, "type preserved") } func TestSlice(t *testing.T) { @@ -687,6 +845,11 @@ func TestSlice(t *testing.T) { is.Equal([]int{0}, out16) is.Equal([]int{0, 1, 2}, out17) is.Equal([]int{0, 1, 2, 3, 4}, out18) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Slice(allStrings, 0, 2) + is.IsType(nonempty, allStrings, "type preserved") } func TestReplace(t *testing.T) { @@ -716,6 +879,11 @@ func TestReplace(t *testing.T) { is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, out8) is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, out9) is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, out10) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Replace(allStrings, "0", "2", 1) + is.IsType(nonempty, allStrings, "type preserved") } func TestReplaceAll(t *testing.T) { @@ -729,6 +897,11 @@ func TestReplaceAll(t *testing.T) { is.Equal([]int{42, 1, 42, 1, 2, 3, 42}, out1) is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, out2) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := ReplaceAll(allStrings, "0", "2") + is.IsType(nonempty, allStrings, "type preserved") } func TestCompact(t *testing.T) { @@ -771,6 +944,11 @@ func TestCompact(t *testing.T) { r5 := Compact([]*foo{&e1, &e2, nil, &e3}) is.Equal(r5, []*foo{&e1, &e2, &e3}) + + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Compact(allStrings) + is.IsType(nonempty, allStrings, "type preserved") } func TestIsSorted(t *testing.T) { @@ -801,3 +979,53 @@ func TestIsSortedByKey(t *testing.T) { return ret })) } + +func TestSplice(t *testing.T) { + t.Parallel() + is := assert.New(t) + + sample := []string{"a", "b", "c", "d", "e", "f", "g"} + + // normal case + results := Splice(sample, 1, "1", "2") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + is.Equal([]string{"a", "1", "2", "b", "c", "d", "e", "f", "g"}, results) + + // check there is no side effect + results = Splice(sample, 1) + results[0] = "b" + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + + // positive overflow + results = Splice(sample, 42, "1", "2") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + is.Equal(results, []string{"a", "b", "c", "d", "e", "f", "g", "1", "2"}) + + // negative overflow + results = Splice(sample, -42, "1", "2") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + is.Equal(results, []string{"1", "2", "a", "b", "c", "d", "e", "f", "g"}) + + // backward + results = Splice(sample, -2, "1", "2") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + is.Equal(results, []string{"a", "b", "c", "d", "e", "1", "2", "f", "g"}) + + results = Splice(sample, -7, "1", "2") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, sample) + is.Equal(results, []string{"1", "2", "a", "b", "c", "d", "e", "f", "g"}) + + // other + is.Equal([]string{"1", "2"}, Splice([]string{}, 0, "1", "2")) + is.Equal([]string{"1", "2"}, Splice([]string{}, 1, "1", "2")) + is.Equal([]string{"1", "2"}, Splice([]string{}, -1, "1", "2")) + is.Equal([]string{"1", "2", "0"}, Splice([]string{"0"}, 0, "1", "2")) + is.Equal([]string{"0", "1", "2"}, Splice([]string{"0"}, 1, "1", "2")) + is.Equal([]string{"1", "2", "0"}, Splice([]string{"0"}, -1, "1", "2")) + + // type preserved + type myStrings []string + allStrings := myStrings{"", "foo", "bar"} + nonempty := Splice(allStrings, 1, "1", "2") + is.IsType(nonempty, allStrings, "type preserved") +} diff --git a/string.go b/string.go index 2606ec86..51b09899 100644 --- a/string.go +++ b/string.go @@ -1,7 +1,8 @@ package lo import ( - "math/rand" + "github.com/samber/lo/internal/rand" + "math" "regexp" "strings" "unicode" @@ -24,6 +25,7 @@ var ( splitWordReg = regexp.MustCompile(`([a-z])([A-Z0-9])|([a-zA-Z])([0-9])|([0-9])([a-zA-Z])|([A-Z])([A-Z])([a-z])`) // bearer:disable go_lang_permissive_regex_validation splitNumberLetterReg = regexp.MustCompile(`([0-9])([a-zA-Z])`) + maximumCapacity = math.MaxInt>>1 + 1 ) // RandomString return a random string. @@ -36,14 +38,53 @@ func RandomString(size int, charset []rune) string { panic("lo.RandomString: Charset parameter must not be empty") } - b := make([]rune, size) - possibleCharactersCount := len(charset) - for i := range b { - // @TODO: Upgrade to math/rand/v2 as soon as we set the minimum Go version to 1.22. - // bearer:disable go_gosec_crypto_weak_random - b[i] = charset[rand.Intn(possibleCharactersCount)] + // see https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go + sb := strings.Builder{} + sb.Grow(size) + // Calculate the number of bits required to represent the charset, + // e.g., for 62 characters, it would need 6 bits (since 62 -> 64 = 2^6) + letterIdBits := int(math.Log2(float64(nearestPowerOfTwo(len(charset))))) + // Determine the corresponding bitmask, + // e.g., for 62 characters, the bitmask would be 111111. + var letterIdMask int64 = 1<= 0; { + // Regenerate the random number if all available bits have been used + if remain == 0 { + cache, remain = rand.Int64(), letterIdMax + } + // Select a character from the charset + if idx := int(cache & letterIdMask); idx < len(charset) { + sb.WriteRune(charset[idx]) + i-- + } + // Shift the bits to the right to prepare for the next character selection, + // e.g., for 62 characters, shift by 6 bits. + cache >>= letterIdBits + // Decrease the remaining number of uses for the current random number. + remain-- } - return string(b) + return sb.String() +} + +// nearestPowerOfTwo returns the nearest power of two. +func nearestPowerOfTwo(cap int) int { + n := cap - 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + if n < 0 { + return 1 + } + if n >= maximumCapacity { + return maximumCapacity + } + return n + 1 } // Substring return part of a string. @@ -59,7 +100,7 @@ func Substring[T ~string](str T, offset int, length uint) T { } } - if offset > size { + if offset >= size { return Empty[T]() } @@ -86,7 +127,7 @@ func ChunkString[T ~string](str T, size int) []T { return []T{str} } - var chunks []T = make([]T, 0, ((len(str)-1)/size)+1) + var chunks = make([]T, 0, ((len(str)-1)/size)+1) currentLen := 0 currentStart := 0 for i := range str { @@ -168,14 +209,23 @@ func Capitalize(str string) string { return cases.Title(language.English).String(str) } -// Elipse truncates a string to a specified length and appends an ellipsis if truncated. -func Elipse(str string, length int) string { +// Ellipsis trims and truncates a string to a specified length and appends an ellipsis if truncated. +func Ellipsis(str string, length int) string { + str = strings.TrimSpace(str) + if len(str) > length { if len(str) < 3 || length < 3 { return "..." } - return str[0:length-3] + "..." + return strings.TrimSpace(str[0:length-3]) + "..." } return str } + +// Elipse trims and truncates a string to a specified length and appends an ellipsis if truncated. +// +// Deprecated: Use Ellipsis instead. +func Elipse(str string, length int) string { + return Ellipsis(str, length) +} diff --git a/string_test.go b/string_test.go index e3d6eabf..fd09ba08 100644 --- a/string_test.go +++ b/string_test.go @@ -76,6 +76,7 @@ func TestSubstring(t *testing.T) { str12 := Substring("hello", -4, math.MaxUint) str13 := Substring("🏠🐢🐱", 0, 2) str14 := Substring("δ½ ε₯½οΌŒδΈ–η•Œ", 0, 3) + str15 := Substring("hello", 5, 1) is.Equal("", str1) is.Equal("", str2) @@ -91,6 +92,7 @@ func TestSubstring(t *testing.T) { is.Equal("ello", str12) is.Equal("🏠🐢", str13) is.Equal("δ½ ε₯½οΌŒ", str14) + is.Equal("", str15) } func TestRuneLength(t *testing.T) { @@ -484,15 +486,18 @@ func TestCapitalize(t *testing.T) { } } -func TestElipse(t *testing.T) { +func TestEllipsis(t *testing.T) { t.Parallel() is := assert.New(t) - is.Equal("12345", Elipse("12345", 5)) - is.Equal("1...", Elipse("12345", 4)) - is.Equal("12345", Elipse("12345", 6)) - is.Equal("12345", Elipse("12345", 10)) - is.Equal("...", Elipse("12345", 3)) - is.Equal("...", Elipse("12345", 2)) - is.Equal("...", Elipse("12345", -1)) + is.Equal("12345", Ellipsis("12345", 5)) + is.Equal("1...", Ellipsis("12345", 4)) + is.Equal("1...", Ellipsis(" 12345 ", 4)) + is.Equal("12345", Ellipsis("12345", 6)) + is.Equal("12345", Ellipsis("12345", 10)) + is.Equal("12345", Ellipsis(" 12345 ", 10)) + is.Equal("...", Ellipsis("12345", 3)) + is.Equal("...", Ellipsis("12345", 2)) + is.Equal("...", Ellipsis("12345", -1)) + is.Equal("hello...", Ellipsis(" hello world ", 9)) } diff --git a/type_manipulation.go b/type_manipulation.go index 3e965183..448abe96 100644 --- a/type_manipulation.go +++ b/type_manipulation.go @@ -58,6 +58,27 @@ func ToSlicePtr[T any](collection []T) []*T { return result } +// FromSlicePtr returns a slice with the pointer values. +// Returns a zero value in case of a nil pointer element. +func FromSlicePtr[T any](collection []*T) []T { + return Map(collection, func(x *T, _ int) T { + if x == nil { + return Empty[T]() + } + return *x + }) +} + +// FromSlicePtrOr returns a slice with the pointer values or the fallback value. +func FromSlicePtrOr[T any](collection []*T, fallback T) []T { + return Map(collection, func(x *T, _ int) T { + if x == nil { + return fallback + } + return *x + }) +} + // ToAnySlice returns a slice with all elements mapped to `any` type func ToAnySlice[T any](collection []T) []any { result := make([]any, len(collection)) diff --git a/type_manipulation_test.go b/type_manipulation_test.go index 0d4ace50..3a63013d 100644 --- a/type_manipulation_test.go +++ b/type_manipulation_test.go @@ -25,9 +25,9 @@ func TestIsNil(t *testing.T) { var b *bool is.True(IsNil(b)) - var ifaceWithNilValue interface{} = (*string)(nil) + var ifaceWithNilValue any = (*string)(nil) //nolint:staticcheck is.True(IsNil(ifaceWithNilValue)) - is.False(ifaceWithNilValue == nil) // nolint:staticcheck + is.False(ifaceWithNilValue == nil) //nolint:staticcheck } func TestToPtr(t *testing.T) { @@ -121,6 +121,26 @@ func TestToSlicePtr(t *testing.T) { is.Equal(result1, []*string{&str1, &str2}) } +func TestFromSlicePtr(t *testing.T) { + is := assert.New(t) + + str1 := "foo" + str2 := "bar" + result1 := FromSlicePtr([]*string{&str1, &str2, nil}) + + is.Equal(result1, []string{str1, str2, ""}) +} + +func TestFromSlicePtrOr(t *testing.T) { + is := assert.New(t) + + str1 := "foo" + str2 := "bar" + result1 := FromSlicePtrOr([]*string{&str1, &str2, nil}, "fallback") + + is.Equal(result1, []string{str1, str2, "fallback"}) +} + func TestToAnySlice(t *testing.T) { t.Parallel() is := assert.New(t)