diff --git a/channel_test.go b/channel_test.go index 7e54630..6157219 100644 --- a/channel_test.go +++ b/channel_test.go @@ -2,7 +2,6 @@ package lo import ( "context" - "math/rand" "testing" "time" @@ -117,9 +116,6 @@ func TestDispatchingStrategyRandom(t *testing.T) { testWithTimeout(t, 10*time.Millisecond) is := assert.New(t) - // with this seed, the order of random channels are: 1 - 0 - rand.Seed(14) - children := createChannels[int](2, 2) rochildren := channelsToReadOnly(children) defer closeChannels(children) diff --git a/find_test.go b/find_test.go index 8e290f8..f5441a6 100644 --- a/find_test.go +++ b/find_test.go @@ -36,6 +36,7 @@ func TestHasPrefix(t *testing.T) { is.True(HasPrefix([]int{1, 2, 3, 4}, []int{1, 2})) is.False(HasPrefix([]int{1, 2, 3, 4}, []int{42})) + is.True(HasPrefix([]int{1, 2, 3, 4}, nil)) } func TestHasSuffix(t *testing.T) { @@ -44,6 +45,7 @@ func TestHasSuffix(t *testing.T) { is.True(HasSuffix([]int{1, 2, 3, 4}, []int{3, 4})) is.False(HasSuffix([]int{1, 2, 3, 4}, []int{42})) + is.True(HasSuffix([]int{1, 2, 3, 4}, nil)) } func TestFind(t *testing.T) { @@ -721,8 +723,6 @@ func TestSample(t *testing.T) { t.Parallel() is := assert.New(t) - rand.Seed(time.Now().UnixNano()) - result1 := Sample([]string{"a", "b", "c"}) result2 := Sample([]string{}) @@ -747,8 +747,6 @@ func TestSamples(t *testing.T) { t.Parallel() is := assert.New(t) - rand.Seed(time.Now().UnixNano()) - result1 := Samples([]string{"a", "b", "c"}, 3) result2 := Samples([]string{}, 3) diff --git a/it/channel.go b/it/channel.go new file mode 100644 index 0000000..2faf69d --- /dev/null +++ b/it/channel.go @@ -0,0 +1,50 @@ +//go:build go1.23 + +package it + +import ( + "iter" + + "github.com/samber/lo" +) + +// SeqToChannel returns a read-only channel of collection elements. +func SeqToChannel[T any](bufferSize int, collection iter.Seq[T]) <-chan T { + ch := make(chan T, bufferSize) + + go func() { + for item := range collection { + ch <- item + } + + close(ch) + }() + + return ch +} + +// SeqToChannel2 returns a read-only channel of collection elements. +func SeqToChannel2[K, V any](bufferSize int, collection iter.Seq2[K, V]) <-chan lo.Tuple2[K, V] { + ch := make(chan lo.Tuple2[K, V], bufferSize) + + go func() { + for k, v := range collection { + ch <- lo.Tuple2[K, V]{A: k, B: v} + } + + close(ch) + }() + + return ch +} + +// ChannelToSeq returns a sequence built from channels items. Blocks until channel closes. +func ChannelToSeq[T any](ch <-chan T) iter.Seq[T] { + return func(yield func(T) bool) { + for item := range ch { + if !yield(item) { + return + } + } + } +} diff --git a/it/channel_test.go b/it/channel_test.go new file mode 100644 index 0000000..c07790e --- /dev/null +++ b/it/channel_test.go @@ -0,0 +1,60 @@ +//go:build go1.23 + +package it + +import ( + "maps" + "slices" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func TestSeqToChannel(t *testing.T) { + t.Parallel() + is := assert.New(t) + + ch := SeqToChannel(2, values(1, 2, 3)) + + r1, ok1 := <-ch + r2, ok2 := <-ch + r3, ok3 := <-ch + is.True(ok1) + is.Equal(1, r1) + is.True(ok2) + is.Equal(2, r2) + is.True(ok3) + is.Equal(3, r3) + + _, ok4 := <-ch + is.False(ok4) +} + +func TestSeqToChannel2(t *testing.T) { + t.Parallel() + is := assert.New(t) + + ch := SeqToChannel2(2, maps.All(map[string]int{"a": 1, "b": 2, "c": 3})) + + r1, ok1 := <-ch + r2, ok2 := <-ch + r3, ok3 := <-ch + is.True(ok1) + is.True(ok2) + is.True(ok3) + is.ElementsMatch([]lo.Tuple2[string, int]{{A: "a", B: 1}, {A: "b", B: 2}, {A: "c", B: 3}}, []lo.Tuple2[string, int]{r1, r2, r3}) + + _, ok4 := <-ch + is.False(ok4) +} + +func TestChannelToSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + ch := SeqToChannel(2, values(1, 2, 3)) + items := ChannelToSeq(ch) + + is.Equal([]int{1, 2, 3}, slices.Collect(items)) +} diff --git a/it/find.go b/it/find.go new file mode 100644 index 0000000..fb66b76 --- /dev/null +++ b/it/find.go @@ -0,0 +1,481 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "iter" + "slices" + "time" + + "github.com/samber/lo" + "github.com/samber/lo/internal/constraints" + "github.com/samber/lo/internal/rand" +) + +// IndexOf returns the index at which the first occurrence of a value is found in a sequence or -1 +// if the value cannot be found. +// Will iterate through the entire sequence if element is not found. +func IndexOf[T comparable](collection iter.Seq[T], element T) int { + var i int + for item := range collection { + if item == element { + return i + } + i++ + } + + return -1 +} + +// LastIndexOf returns the index at which the last occurrence of a value is found in a sequence or -1 +// if the value cannot be found. +// Will iterate through the entire sequence. +func LastIndexOf[T comparable](collection iter.Seq[T], element T) int { + index := -1 + var i int + for item := range collection { + if item == element { + index = i + } + i++ + } + + return index +} + +// HasPrefix returns true if the collection has the prefix. +// Will iterate at most the size of prefix. +func HasPrefix[T comparable](collection iter.Seq[T], prefix ...T) bool { + if len(prefix) == 0 { + return true + } + + var i int + + for item := range collection { + if item != prefix[i] { + return false + } + i++ + if i == len(prefix) { + return true + } + } + + return false +} + +// HasSuffix returns true if the collection has the suffix. +// Will iterate through the entire sequence and allocate a slice the size of suffix. +func HasSuffix[T comparable](collection iter.Seq[T], suffix ...T) bool { + if len(suffix) == 0 { + return true + } + + n := len(suffix) + buf := make([]T, 0, n) + var i int + + for item := range collection { + if len(buf) < n { + buf = append(buf, item) + } else { + buf[i] = item + } + i = (i + 1) % n + } + + if len(buf) < n { + return false + } + + i += n + for j := range suffix { + if suffix[j] != buf[(i+j)%n] { + return false + } + } + + return true +} + +// Find searches for an element in a sequence based on a predicate. Returns element and true if element was found. +// Will iterate through the entire sequence if predicate never returns true. +func Find[T any](collection iter.Seq[T], predicate func(item T) bool) (T, bool) { + return First(Filter(collection, predicate)) +} + +// FindIndexOf searches for an element in a sequence based on a predicate and returns the index and true. +// Returns -1 and false if the element is not found. +// Will iterate through the entire sequence if predicate never returns true. +func FindIndexOf[T any](collection iter.Seq[T], predicate func(item T) bool) (T, int, bool) { + var i int + for item := range collection { + if predicate(item) { + return item, i, true + } + i++ + } + + return lo.Empty[T](), -1, false +} + +// FindLastIndexOf searches for the last element in a sequence based on a predicate and returns the index and true. +// Returns -1 and false if the element is not found. +// Will iterate through the entire sequence. +func FindLastIndexOf[T any](collection iter.Seq[T], predicate func(item T) bool) (T, int, bool) { + var result T + index := -1 + var ok bool + + var i int + for item := range collection { + if predicate(item) { + result = item + index = i + ok = true + } + i++ + } + + return result, index, ok +} + +// FindOrElse searches for an element in a sequence based on a predicate. Returns the element if found or a given fallback value otherwise. +// Will iterate through the entire sequence if predicate never returns true. +func FindOrElse[T any](collection iter.Seq[T], fallback T, predicate func(item T) bool) T { + if result, ok := Find(collection, predicate); ok { + return result + } + + return fallback +} + +// FindUniques returns a sequence with all the elements that appear in the collection only once. +// The order of result values is determined by the order they occur in the collection. +// Will iterate through the entire sequence before yielding and allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func FindUniques[T comparable, I ~func(func(T) bool)](collection I) I { + return FindUniquesBy(collection, func(item T) T { return item }) +} + +// FindUniquesBy returns a sequence with all the elements that appear in the collection only once. +// The order of result values is determined by the order they occur in the sequence. A transform function is +// invoked for each element in the sequence to generate the criterion by which uniqueness is computed. +// Will iterate through the entire sequence before yielding and allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func FindUniquesBy[T any, U comparable, I ~func(func(T) bool)](collection I, transform func(item T) U) I { + return func(yield func(T) bool) { + isDupl := make(map[U]bool) + + for item := range collection { + key := transform(item) + + if duplicated, ok := isDupl[key]; !ok { + isDupl[key] = false + } else if !duplicated { + isDupl[key] = true + } + } + + for item := range collection { + key := transform(item) + + if duplicated := isDupl[key]; !duplicated && !yield(item) { + return + } + } + } +} + +// FindDuplicates returns a sequence with the first occurrence of each duplicated element in the collection. +// The order of result values is determined by the order duplicates occur in the collection. +// Will allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func FindDuplicates[T comparable, I ~func(func(T) bool)](collection I) I { + return FindDuplicatesBy(collection, func(item T) T { return item }) +} + +// FindDuplicatesBy returns a sequence with the first occurrence of each duplicated element in the collection. +// The order of result values is determined by the order duplicates occur in the sequence. A transform function is +// invoked for each element in the sequence to generate the criterion by which uniqueness is computed. +// Will allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func FindDuplicatesBy[T any, U comparable, I ~func(func(T) bool)](collection I, transform func(item T) U) I { + return func(yield func(T) bool) { + isDupl := make(map[U]lo.Tuple2[T, bool]) + + for item := range collection { + key := transform(item) + + if duplicated, ok := isDupl[key]; !ok { + isDupl[key] = lo.Tuple2[T, bool]{A: item} + } else if !duplicated.B { + if !yield(duplicated.A) { + return + } + isDupl[key] = lo.Tuple2[T, bool]{A: item, B: true} + } + } + } +} + +// Min search the minimum value of a collection. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func Min[T constraints.Ordered](collection iter.Seq[T]) T { + return MinBy(collection, func(a, b T) bool { return a < b }) +} + +// MinIndex search the minimum value of a collection and the index of the minimum value. +// Returns (zero value, -1) when the collection is empty. +// Will iterate through the entire sequence. +func MinIndex[T constraints.Ordered](collection iter.Seq[T]) (T, int) { + return MinIndexBy(collection, func(a, b T) bool { return a < b }) +} + +// MinBy search the minimum value of a collection using the given comparison function. +// If several values of the collection are equal to the smallest value, returns the first such value. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func MinBy[T any](collection iter.Seq[T], comparison func(a, b T) bool) T { + first := true + var mIn T + + for item := range collection { + if first { + mIn = item + first = false + } else if comparison(item, mIn) { + mIn = item + } + } + + return mIn +} + +// MinIndexBy search the minimum value of a collection using the given comparison function and the index of the minimum value. +// If several values of the collection are equal to the smallest value, returns the first such value. +// Returns (zero value, -1) when the collection is empty. +// Will iterate through the entire sequence. +func MinIndexBy[T any](collection iter.Seq[T], comparison func(a, b T) bool) (T, int) { + var mIn T + index := -1 + + var i int + for item := range collection { + if i == 0 || comparison(item, mIn) { + mIn = item + index = i + } + i++ + } + + return mIn, index +} + +// Earliest search the minimum time.Time of a collection. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func Earliest(times iter.Seq[time.Time]) time.Time { + return MinBy(times, func(a, b time.Time) bool { return a.Before(b) }) +} + +// EarliestBy search the minimum time.Time of a collection using the given transform function. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func EarliestBy[T any](collection iter.Seq[T], transform func(item T) time.Time) T { + return MinBy(collection, func(a, b T) bool { return transform(a).Before(transform(b)) }) +} + +// Max searches the maximum value of a collection. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func Max[T constraints.Ordered](collection iter.Seq[T]) T { + return MaxBy(collection, func(a, b T) bool { return a > b }) +} + +// MaxIndex searches the maximum value of a collection and the index of the maximum value. +// Returns (zero value, -1) when the collection is empty. +// Will iterate through the entire sequence. +func MaxIndex[T constraints.Ordered](collection iter.Seq[T]) (T, int) { + return MaxIndexBy(collection, func(a, b T) bool { return a > b }) +} + +// MaxBy search the maximum value of a collection using the given comparison function. +// If several values of the collection are equal to the greatest value, returns the first such value. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func MaxBy[T any](collection iter.Seq[T], comparison func(a, b T) bool) T { + first := true + var mAx T + + for item := range collection { + if first { + mAx = item + first = false + } else if comparison(item, mAx) { + mAx = item + } + } + + return mAx +} + +// MaxIndexBy search the maximum value of a collection using the given comparison function and the index of the maximum value. +// If several values of the collection are equal to the greatest value, returns the first such value. +// Returns (zero value, -1) when the collection is empty. +// Will iterate through the entire sequence. +func MaxIndexBy[T any](collection iter.Seq[T], comparison func(a, b T) bool) (T, int) { + var mAx T + index := -1 + + var i int + for item := range collection { + if i == 0 || comparison(item, mAx) { + mAx = item + index = i + } + i++ + } + + return mAx, index +} + +// Latest search the maximum time.Time of a collection. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func Latest(times iter.Seq[time.Time]) time.Time { + return MaxBy(times, func(a, b time.Time) bool { return a.After(b) }) +} + +// LatestBy search the maximum time.Time of a collection using the given transform function. +// Returns zero value when the collection is empty. +// Will iterate through the entire sequence. +func LatestBy[T any](collection iter.Seq[T], transform func(item T) time.Time) T { + return MaxBy(collection, func(a, b T) bool { return transform(a).After(transform(b)) }) +} + +// First returns the first element of a collection and check for availability of the first element. +// Will iterate at most once. +func First[T any](collection iter.Seq[T]) (T, bool) { + for item := range collection { + return item, true + } + + return lo.Empty[T](), false +} + +// FirstOrEmpty returns the first element of a collection or zero value if empty. +// Will iterate at most once. +func FirstOrEmpty[T any](collection iter.Seq[T]) T { + i, _ := First(collection) + return i +} + +// FirstOr returns the first element of a collection or the fallback value if empty. +// Will iterate at most once. +func FirstOr[T any](collection iter.Seq[T], fallback T) T { + if i, ok := First(collection); ok { + return i + } + + return fallback +} + +// Last returns the last element of a collection or error if empty. +// Will iterate through the entire sequence. +func Last[T any](collection iter.Seq[T]) (T, bool) { + var t T + var ok bool + for item := range collection { + t = item + ok = true + } + + return t, ok +} + +// LastOrEmpty returns the last element of a collection or zero value if empty. +// Will iterate through the entire sequence. +func LastOrEmpty[T any](collection iter.Seq[T]) T { + i, _ := Last(collection) + return i +} + +// LastOr returns the last element of a collection or the fallback value if empty. +// Will iterate through the entire sequence. +func LastOr[T any](collection iter.Seq[T], fallback T) T { + if i, ok := Last(collection); ok { + return i + } + + return fallback +} + +// Nth returns the element at index `nth` of collection. An error is returned when nth is out of bounds. +// Will iterate n times through the sequence. +func Nth[T any, N constraints.Integer](collection iter.Seq[T], nth N) (T, error) { + if nth >= 0 { + var i N + for item := range collection { + if i == nth { + return item, nil + } + i++ + } + } + + return lo.Empty[T](), fmt.Errorf("nth: %d out of bounds", nth) +} + +// NthOr returns the element at index `nth` of collection. +// If `nth` is out of bounds, it returns the fallback value instead of an error. +// Will iterate n times through the sequence. +func NthOr[T any, N constraints.Integer](collection iter.Seq[T], nth N, fallback T) T { + value, err := Nth(collection, nth) + if err != nil { + return fallback + } + return value +} + +// NthOrEmpty returns the element at index `nth` of collection. +// If `nth` is out of bounds, it returns the zero value (empty value) for that type. +// Will iterate n times through the sequence. +func NthOrEmpty[T any, N constraints.Integer](collection iter.Seq[T], nth N) T { + value, _ := Nth(collection, nth) + return value +} + +// Sample returns a random item from collection. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func Sample[T any](collection iter.Seq[T]) T { + return SampleBy(collection, rand.IntN) +} + +// SampleBy returns a random item from collection, using randomIntGenerator as the random index generator. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func SampleBy[T any](collection iter.Seq[T], randomIntGenerator func(int) int) T { + slice := slices.Collect(collection) + return lo.SampleBy(slice, randomIntGenerator) +} + +// Samples returns N random unique items from collection. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func Samples[T any, I ~func(func(T) bool)](collection I, count int) I { + return SamplesBy(collection, count, rand.IntN) +} + +// SamplesBy returns N random unique items from collection, using randomIntGenerator as the random index generator. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func SamplesBy[T any, I ~func(func(T) bool)](collection I, count int, randomIntGenerator func(int) int) I { + slice := slices.Collect(iter.Seq[T](collection)) + seq := lo.SamplesBy(slice, count, randomIntGenerator) + return I(slices.Values(seq)) +} diff --git a/it/find_example_test.go b/it/find_example_test.go new file mode 100644 index 0000000..36f7e5c --- /dev/null +++ b/it/find_example_test.go @@ -0,0 +1,582 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "slices" + "time" +) + +func ExampleIndexOf() { + list := slices.Values([]string{"foo", "bar", "baz"}) + + result := IndexOf(list, "bar") + + fmt.Printf("%d", result) + // Output: 1 +} + +func ExampleIndexOf_notFound() { + list := slices.Values([]string{"foo", "bar", "baz"}) + + result := IndexOf(list, "qux") + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleLastIndexOf() { + list := slices.Values([]string{"foo", "bar", "baz", "bar"}) + + result := LastIndexOf(list, "bar") + + fmt.Printf("%d", result) + // Output: 3 +} + +func ExampleLastIndexOf_notFound() { + list := slices.Values([]string{"foo", "bar", "baz"}) + + result := LastIndexOf(list, "qux") + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleFind() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }) + + result, found := Find(users, func(user User) bool { + return user.Age > 30 + }) + + fmt.Printf("%s %t", result.Name, found) + // Output: Charlie true +} + +func ExampleFind_notFound() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, found := Find(list, func(n int) bool { + return n > 10 + }) + + fmt.Printf("%d %t", result, found) + // Output: 0 false +} + +func ExampleFindIndexOf() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, index, found := FindIndexOf(list, func(n int) bool { + return n > 2 + }) + + fmt.Printf("%d %d %t", result, index, found) + // Output: 3 2 true +} + +func ExampleFindIndexOf_notFound() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, index, found := FindIndexOf(list, func(n int) bool { + return n > 10 + }) + + fmt.Printf("%d %d %t", result, index, found) + // Output: 0 -1 false +} + +func ExampleFindLastIndexOf() { + list := slices.Values([]int{1, 2, 3, 4, 3, 5}) + + result, index, found := FindLastIndexOf(list, func(n int) bool { + return n == 3 + }) + + fmt.Printf("%d %d %t", result, index, found) + // Output: 3 4 true +} + +func ExampleFindLastIndexOf_notFound() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, index, found := FindLastIndexOf(list, func(n int) bool { + return n > 10 + }) + + fmt.Printf("%d %d %t", result, index, found) + // Output: 0 -1 false +} + +func ExampleFindOrElse() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := FindOrElse(list, -1, func(n int) bool { + return n > 10 + }) + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleFindOrElse_found() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := FindOrElse(list, -1, func(n int) bool { + return n > 3 + }) + + fmt.Printf("%d", result) + // Output: 4 +} + +func ExampleFindUniques() { + list := slices.Values([]int{1, 2, 2, 3, 3, 3, 4, 5}) + + result := FindUniques(list) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [1 4 5] +} + +func ExampleFindUniquesBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 25}, + {Name: "David", Age: 30}, + {Name: "Eve", Age: 35}, + }) + + result := FindUniquesBy(users, func(user User) int { + return user.Age + }) + + fmt.Printf("%d", len(slices.Collect(result))) + // Output: 1 +} + +func ExampleFindDuplicates() { + list := slices.Values([]int{1, 2, 2, 3, 3, 3, 4, 5}) + + result := FindDuplicates(list) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 3] +} + +func ExampleFindDuplicatesBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 25}, + {Name: "David", Age: 30}, + {Name: "Eve", Age: 35}, + }) + + result := FindDuplicatesBy(users, func(user User) int { + return user.Age + }) + + fmt.Printf("%d", len(slices.Collect(result))) + // Output: 2 +} + +func ExampleMin() { + list := slices.Values([]int{3, 1, 4, 1, 5, 9, 2, 6}) + + result := Min(list) + + fmt.Printf("%d", result) + // Output: 1 +} + +func ExampleMin_empty() { + list := slices.Values([]int{}) + + result := Min(list) + + fmt.Printf("%d", result) + // Output: 0 +} + +func ExampleMinIndex() { + list := slices.Values([]int{3, 1, 4, 1, 5, 9, 2, 6}) + + result, index := MinIndex(list) + + fmt.Printf("%d %d", result, index) + // Output: 1 1 +} + +func ExampleMinIndex_empty() { + list := slices.Values([]int{}) + + result, index := MinIndex(list) + + fmt.Printf("%d %d", result, index) + // Output: 0 -1 +} + +func ExampleMinBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }) + + result := MinBy(users, func(a, b User) bool { + return a.Age < b.Age + }) + + fmt.Printf("%s", result.Name) + // Output: Alice +} + +func ExampleMinIndexBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }) + + result, index := MinIndexBy(users, func(a, b User) bool { + return a.Age < b.Age + }) + + fmt.Printf("%s %d", result.Name, index) + // Output: Alice 0 +} + +func ExampleEarliest() { + now := time.Now() + past := now.Add(-time.Hour) + future := now.Add(time.Hour) + + result := Earliest(slices.Values([]time.Time{future, now, past})) + + fmt.Printf("%t", result.Equal(past)) + // Output: true +} + +func ExampleEarliestBy() { + type Event struct { + Name string + Time time.Time + } + + now := time.Now() + events := slices.Values([]Event{ + {Name: "Event A", Time: now.Add(time.Hour)}, + {Name: "Event B", Time: now}, + {Name: "Event C", Time: now.Add(-time.Hour)}, + }) + + result := EarliestBy(events, func(event Event) time.Time { + return event.Time + }) + + fmt.Printf("%s", result.Name) + // Output: Event C +} + +func ExampleMax() { + list := slices.Values([]int{3, 1, 4, 1, 5, 9, 2, 6}) + + result := Max(list) + + fmt.Printf("%d", result) + // Output: 9 +} + +func ExampleMax_empty() { + list := slices.Values([]int{}) + + result := Max(list) + + fmt.Printf("%d", result) + // Output: 0 +} + +func ExampleMaxIndex() { + list := slices.Values([]int{3, 1, 4, 1, 5, 9, 2, 6}) + + result, index := MaxIndex(list) + + fmt.Printf("%d %d", result, index) + // Output: 9 5 +} + +func ExampleMaxIndex_empty() { + list := slices.Values([]int{}) + + result, index := MaxIndex(list) + + fmt.Printf("%d %d", result, index) + // Output: 0 -1 +} + +func ExampleMaxBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }) + + result := MaxBy(users, func(a, b User) bool { + return a.Age > b.Age + }) + + fmt.Printf("%s", result.Name) + // Output: Charlie +} + +func ExampleMaxIndexBy() { + type User struct { + Name string + Age int + } + + users := slices.Values([]User{ + {Name: "Alice", Age: 25}, + {Name: "Bob", Age: 30}, + {Name: "Charlie", Age: 35}, + }) + + result, index := MaxIndexBy(users, func(a, b User) bool { + return a.Age > b.Age + }) + + fmt.Printf("%s %d", result.Name, index) + // Output: Charlie 2 +} + +func ExampleLatest() { + now := time.Now() + past := now.Add(-time.Hour) + future := now.Add(time.Hour) + + result := Latest(slices.Values([]time.Time{future, now, past})) + + fmt.Printf("%t", result.Equal(future)) + // Output: true +} + +func ExampleLatestBy() { + type Event struct { + Name string + Time time.Time + } + + now := time.Now() + events := slices.Values([]Event{ + {Name: "Event A", Time: now.Add(time.Hour)}, + {Name: "Event B", Time: now}, + {Name: "Event C", Time: now.Add(-time.Hour)}, + }) + + result := LatestBy(events, func(event Event) time.Time { + return event.Time + }) + + fmt.Printf("%s", result.Name) + // Output: Event A +} + +func ExampleFirst() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, found := First(list) + + fmt.Printf("%d %t", result, found) + // Output: 1 true +} + +func ExampleFirst_empty() { + list := slices.Values([]int{}) + + result, found := First(list) + + fmt.Printf("%d %t", result, found) + // Output: 0 false +} + +func ExampleFirstOrEmpty() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := FirstOrEmpty(list) + + fmt.Printf("%d", result) + // Output: 1 +} + +func ExampleFirstOrEmpty_empty() { + list := slices.Values([]int{}) + + result := FirstOrEmpty(list) + + fmt.Printf("%d", result) + // Output: 0 +} + +func ExampleFirstOr() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := FirstOr(list, -1) + + fmt.Printf("%d", result) + // Output: 1 +} + +func ExampleFirstOr_empty() { + list := slices.Values([]int{}) + + result := FirstOr(list, -1) + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleLast() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, found := Last(list) + + fmt.Printf("%d %t", result, found) + // Output: 5 true +} + +func ExampleLast_empty() { + list := slices.Values([]int{}) + + result, found := Last(list) + + fmt.Printf("%d %t", result, found) + // Output: 0 false +} + +func ExampleLastOrEmpty() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := LastOrEmpty(list) + + fmt.Printf("%d", result) + // Output: 5 +} + +func ExampleLastOrEmpty_empty() { + list := slices.Values([]int{}) + + result := LastOrEmpty(list) + + fmt.Printf("%d", result) + // Output: 0 +} + +func ExampleLastOr() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := LastOr(list, -1) + + fmt.Printf("%d", result) + // Output: 5 +} + +func ExampleLastOr_empty() { + list := slices.Values([]int{}) + + result := LastOr(list, -1) + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleNth() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, err := Nth(list, 2) + + fmt.Printf("%d %v", result, err) + // Output: 3 +} + +func ExampleNth_outOfBounds() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result, err := Nth(list, 10) + + fmt.Printf("%d %v", result, err) + // Output: 0 nth: 10 out of bounds +} + +func ExampleNthOr() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := NthOr(list, 2, -1) + + fmt.Printf("%d", result) + // Output: 3 +} + +func ExampleNthOr_outOfBounds() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := NthOr(list, 10, -1) + + fmt.Printf("%d", result) + // Output: -1 +} + +func ExampleNthOrEmpty() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := NthOrEmpty(list, 2) + + fmt.Printf("%d", result) + // Output: 3 +} + +func ExampleNthOrEmpty_outOfBounds() { + list := slices.Values([]int{1, 2, 3, 4, 5}) + + result := NthOrEmpty(list, 10) + + fmt.Printf("%d", result) + // Output: 0 +} diff --git a/it/find_test.go b/it/find_test.go new file mode 100644 index 0000000..44a4f7a --- /dev/null +++ b/it/find_test.go @@ -0,0 +1,727 @@ +//go:build go1.23 + +package it + +import ( + "iter" + "math/rand/v2" + "slices" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestIndexOf(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := IndexOf(values(0, 1, 2, 1, 2, 3), 2) + result2 := IndexOf(values(0, 1, 2, 1, 2, 3), 6) + + is.Equal(2, result1) + is.Equal(-1, result2) +} + +func TestLastIndexOf(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := LastIndexOf(values(0, 1, 2, 1, 2, 3), 2) + result2 := LastIndexOf(values(0, 1, 2, 1, 2, 3), 6) + + is.Equal(4, result1) + is.Equal(-1, result2) +} + +func TestHasPrefix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.True(HasPrefix(values(1, 2, 3, 4), 1, 2)) + is.False(HasPrefix(values(1, 2, 3, 4), 42)) + is.False(HasPrefix(values(1, 2), 1, 2, 3, 4)) + is.True(HasPrefix(values(1, 2, 3, 4))) +} + +func TestHasSuffix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.True(HasSuffix(values(1, 2, 3, 4), 3, 4)) + is.True(HasSuffix(values(1, 2, 3, 4, 5), 3, 4, 5)) + is.False(HasSuffix(values(1, 2, 3, 4), 42)) + is.False(HasSuffix(values(1, 2), 1, 2, 3, 4)) + is.True(HasSuffix(values(1, 2, 3, 4))) +} + +func TestFind(t *testing.T) { + t.Parallel() + is := assert.New(t) + + index := 0 + result1, ok1 := Find(values("a", "b", "c", "d"), func(item string) bool { + is.Equal([]string{"a", "b", "c", "d"}[index], item) + index++ + return item == "b" + }) + + result2, ok2 := Find(values("foobar"), func(item string) bool { + is.Equal("foobar", item) + return item == "b" + }) + + is.True(ok1) + is.Equal("b", result1) + is.False(ok2) + is.Empty(result2) +} + +func TestFindIndexOf(t *testing.T) { + t.Parallel() + is := assert.New(t) + + index := 0 + item1, index1, ok1 := FindIndexOf(values("a", "b", "c", "d", "b"), func(item string) bool { + is.Equal([]string{"a", "b", "c", "d", "b"}[index], item) + index++ + return item == "b" + }) + item2, index2, ok2 := FindIndexOf(values("foobar"), func(item string) bool { + is.Equal("foobar", item) + return item == "b" + }) + + is.Equal("b", item1) + is.True(ok1) + is.Equal(1, index1) + is.Empty(item2) + is.False(ok2) + is.Equal(-1, index2) +} + +func TestFindLastIndexOf(t *testing.T) { + t.Parallel() + is := assert.New(t) + + item1, index1, ok1 := FindLastIndexOf(values("a", "b", "c", "d", "b"), func(item string) bool { + return item == "b" + }) + item2, index2, ok2 := FindLastIndexOf(values("foobar"), func(item string) bool { + return item == "b" + }) + + is.Equal("b", item1) + is.True(ok1) + is.Equal(4, index1) + is.Empty(item2) + is.False(ok2) + is.Equal(-1, index2) +} + +func TestFindOrElse(t *testing.T) { + t.Parallel() + is := assert.New(t) + + index := 0 + result1 := FindOrElse(values("a", "b", "c", "d"), "x", func(item string) bool { + is.Equal([]string{"a", "b", "c", "d"}[index], item) + index++ + return item == "b" + }) + result2 := FindOrElse(values("foobar"), "x", func(item string) bool { + is.Equal("foobar", item) + return item == "b" + }) + + is.Equal("b", result1) + is.Equal("x", result2) +} + +func TestFindUniques(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FindUniques(values(1, 2, 3)) + is.Equal([]int{1, 2, 3}, slices.Collect(result1)) + + result2 := FindUniques(values(1, 2, 2, 3, 1, 2)) + is.Equal([]int{3}, slices.Collect(result2)) + + result3 := FindUniques(values(1, 2, 2, 1)) + is.Empty(slices.Collect(result3)) + + result4 := FindUniques(values[int]()) + is.Empty(slices.Collect(result4)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := FindUniques(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestFindUniquesBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FindUniquesBy(values(0, 1, 2), func(i int) int { + return i % 3 + }) + is.Equal([]int{0, 1, 2}, slices.Collect(result1)) + + result2 := FindUniquesBy(values(0, 1, 2, 3, 4), func(i int) int { + return i % 3 + }) + is.Equal([]int{2}, slices.Collect(result2)) + + result3 := FindUniquesBy(values(0, 1, 2, 3, 4, 5), func(i int) int { + return i % 3 + }) + is.Empty(slices.Collect(result3)) + + result4 := FindUniquesBy(values[int](), func(i int) int { + return i % 3 + }) + is.Empty(slices.Collect(result4)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := FindUniquesBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestFindDuplicates(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FindDuplicates(values(1, 2, 2, 1, 2, 3)) + is.Equal([]int{2, 1}, slices.Collect(result1)) + + result2 := FindDuplicates(values(1, 2, 3)) + is.Empty(slices.Collect(result2)) + + result3 := FindDuplicates(values[int]()) + is.Empty(slices.Collect(result3)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := FindDuplicates(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestFindDuplicatesBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FindDuplicatesBy(values(3, 4, 5, 6, 7), func(i int) int { + return i % 3 + }) + is.Equal([]int{3, 4}, slices.Collect(result1)) + + result2 := FindDuplicatesBy(values(0, 1, 2, 3, 4), func(i int) int { + return i % 5 + }) + is.Empty(slices.Collect(result2)) + + result3 := FindDuplicatesBy(values[int](), func(i int) int { + return i % 3 + }) + is.Empty(slices.Collect(result3)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := FindDuplicatesBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestMin(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Min(values(1, 2, 3)) + result2 := Min(values(3, 2, 1)) + result3 := Min(values(time.Second, time.Minute, time.Hour)) + result4 := Min(values[int]()) + + is.Equal(1, result1) + is.Equal(1, result2) + is.Equal(time.Second, result3) + is.Zero(result4) +} + +func TestMinIndex(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, index1 := MinIndex(values(1, 2, 3)) + result2, index2 := MinIndex(values(3, 2, 1)) + result3, index3 := MinIndex(values(time.Second, time.Minute, time.Hour)) + result4, index4 := MinIndex(values[int]()) + + is.Equal(1, result1) + is.Zero(index1) + + is.Equal(1, result2) + is.Equal(2, index2) + + is.Equal(time.Second, result3) + is.Zero(index3) + + is.Zero(result4) + is.Equal(-1, index4) +} + +func TestMinBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := MinBy(values("s1", "string2", "s3"), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + result2 := MinBy(values("string1", "string2", "s3"), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + result3 := MinBy(values[string](), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + + is.Equal("s1", result1) + is.Equal("s3", result2) + is.Empty(result3) +} + +func TestMinIndexBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, index1 := MinIndexBy(values("s1", "string2", "s3"), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + result2, index2 := MinIndexBy(values("string1", "string2", "s3"), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + result3, index3 := MinIndexBy(values[string](), func(item, mIn string) bool { + return len(item) < len(mIn) + }) + + is.Equal("s1", result1) + is.Zero(index1) + + is.Equal("s3", result2) + is.Equal(2, index2) + + is.Empty(result3) + is.Equal(-1, index3) +} + +func TestEarliest(t *testing.T) { + t.Parallel() + is := assert.New(t) + + a := time.Now() + b := a.Add(time.Hour) + result1 := Earliest(values(a, b)) + result2 := Earliest(values[time.Time]()) + + is.Equal(a, result1) + is.Zero(result2) +} + +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(values(foo{t1}, foo{t2}, foo{t3}), func(i foo) time.Time { + return i.bar + }) + result2 := EarliestBy(values(foo{t1}), func(i foo) time.Time { + return i.bar + }) + result3 := EarliestBy(values[foo](), func(i foo) time.Time { + return i.bar + }) + + is.Equal(foo{t3}, result1) + is.Equal(foo{t1}, result2) + is.Zero(result3) +} + +func TestMax(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Max(values(1, 2, 3)) + result2 := Max(values(3, 2, 1)) + result3 := Max(values(time.Second, time.Minute, time.Hour)) + result4 := Max(values[int]()) + + is.Equal(3, result1) + is.Equal(3, result2) + is.Equal(time.Hour, result3) + is.Zero(result4) +} + +func TestMaxIndex(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, index1 := MaxIndex(values(1, 2, 3)) + result2, index2 := MaxIndex(values(3, 2, 1)) + result3, index3 := MaxIndex(values(time.Second, time.Minute, time.Hour)) + result4, index4 := MaxIndex(values[int]()) + + is.Equal(3, result1) + is.Equal(2, index1) + + is.Equal(3, result2) + is.Zero(index2) + + is.Equal(time.Hour, result3) + is.Equal(2, index3) + + is.Zero(result4) + is.Equal(-1, index4) +} + +func TestMaxBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := MaxBy(values("s1", "string2", "s3"), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + result2 := MaxBy(values("string1", "string2", "s3"), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + result3 := MaxBy(values[string](), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + + is.Equal("string2", result1) + is.Equal("string1", result2) + is.Empty(result3) +} + +func TestMaxIndexBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, index1 := MaxIndexBy(values("s1", "string2", "s3"), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + result2, index2 := MaxIndexBy(values("string1", "string2", "s3"), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + result3, index3 := MaxIndexBy(values[string](), func(item, mAx string) bool { + return len(item) > len(mAx) + }) + + is.Equal("string2", result1) + is.Equal(1, index1) + + is.Equal("string1", result2) + is.Zero(index2) + + is.Empty(result3) + is.Equal(-1, index3) +} + +func TestLatest(t *testing.T) { + t.Parallel() + is := assert.New(t) + + a := time.Now() + b := a.Add(time.Hour) + result1 := Latest(values(a, b)) + result2 := Latest(values[time.Time]()) + + is.Equal(b, result1) + is.Zero(result2) +} + +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(values(foo{t1}, foo{t2}, foo{t3}), func(i foo) time.Time { + return i.bar + }) + result2 := LatestBy(values(foo{t1}), func(i foo) time.Time { + return i.bar + }) + result3 := LatestBy(values[foo](), func(i foo) time.Time { + return i.bar + }) + + is.Equal(foo{t2}, result1) + is.Equal(foo{t1}, result2) + is.Zero(result3) +} + +func TestFirst(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, ok1 := First(values(1, 2, 3)) + result2, ok2 := First(values[int]()) + + is.Equal(1, result1) + is.True(ok1) + is.Zero(result2) + is.False(ok2) +} + +func TestFirstOrEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FirstOrEmpty(values(1, 2, 3)) + result2 := FirstOrEmpty(values[int]()) + result3 := FirstOrEmpty(values[string]()) + + is.Equal(1, result1) + is.Zero(result2) + is.Empty(result3) +} + +func TestFirstOr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FirstOr(values(1, 2, 3), 63) + result2 := FirstOr(values[int](), 23) + result3 := FirstOr(values[string](), "test") + + is.Equal(1, result1) + is.Equal(23, result2) + is.Equal("test", result3) +} + +func TestLast(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, ok1 := Last(values(1, 2, 3)) + result2, ok2 := Last(values[int]()) + + is.Equal(3, result1) + is.True(ok1) + is.Zero(result2) + is.False(ok2) +} + +func TestLastOrEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := LastOrEmpty(values(1, 2, 3)) + result2 := LastOrEmpty(values[int]()) + result3 := LastOrEmpty(values[string]()) + + is.Equal(3, result1) + is.Zero(result2) + is.Empty(result3) +} + +func TestLastOr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := LastOr(values(1, 2, 3), 63) + result2 := LastOr(values[int](), 23) + result3 := LastOr(values[string](), "test") + + is.Equal(3, result1) + is.Equal(23, result2) + is.Equal("test", result3) +} + +func TestNth(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1, err1 := Nth(values(0, 1, 2, 3), 2) + result2, err2 := Nth(values(0, 1, 2, 3), -2) + result3, err3 := Nth(values(0, 1, 2, 3), 42) + result4, err4 := Nth(values[int](), 0) + result5, err5 := Nth(values(42), 0) + result6, err6 := Nth(values(42), -1) + + is.Equal(2, result1) + is.NoError(err1) + is.Zero(result2) + is.EqualError(err2, "nth: -2 out of bounds") + is.Zero(result3) + is.EqualError(err3, "nth: 42 out of bounds") + is.Zero(result4) + is.EqualError(err4, "nth: 0 out of bounds") + is.Equal(42, result5) + is.NoError(err5) + is.Zero(result6) + is.EqualError(err6, "nth: -1 out of bounds") +} + +func TestNthOr(t *testing.T) { + t.Parallel() + + t.Run("Integers", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + const defaultValue = -1 + ints := values(10, 20, 30, 40, 50) + + is.Equal(30, NthOr(ints, 2, defaultValue)) + is.Equal(defaultValue, NthOr(ints, -1, defaultValue)) + is.Equal(defaultValue, NthOr(ints, 5, defaultValue)) + }) + + t.Run("Strings", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + const defaultValue = "none" + strs := values("apple", "banana", "cherry", "date") + + is.Equal("banana", NthOr(strs, 1, defaultValue)) // Index 1, expected "banana" + is.Equal(defaultValue, NthOr(strs, -2, defaultValue)) // Negative index -2, expected "cherry" + is.Equal(defaultValue, NthOr(strs, 10, defaultValue)) // Out of bounds, fallback "none" + }) + + t.Run("Structs", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + ID int + Name string + } + users := values( + User{ID: 1, Name: "Alice"}, + User{ID: 2, Name: "Bob"}, + User{ID: 3, Name: "Charlie"}, + ) + defaultValue := User{ID: 0, Name: "Unknown"} + + is.Equal(User{ID: 1, Name: "Alice"}, NthOr(users, 0, defaultValue)) + is.Equal(defaultValue, NthOr(users, -1, defaultValue)) + is.Equal(defaultValue, NthOr(users, 10, defaultValue)) + }) +} + +func TestNthOrEmpty(t *testing.T) { + t.Parallel() + + t.Run("Integers", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + ints := values(10, 20, 30, 40, 50) + + is.Equal(30, NthOrEmpty(ints, 2)) + is.Zero(NthOrEmpty(ints, -1)) + is.Zero(NthOrEmpty(ints, 10)) + }) + + t.Run("Strings", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + strs := values("apple", "banana", "cherry", "date") + + is.Equal("banana", NthOrEmpty(strs, 1)) + is.Empty(NthOrEmpty(strs, -2)) + is.Empty(NthOrEmpty(strs, 10)) + }) + + t.Run("Structs", func(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + ID int + Name string + } + users := values( + User{ID: 1, Name: "Alice"}, + User{ID: 2, Name: "Bob"}, + User{ID: 3, Name: "Charlie"}, + ) + + is.Equal(User{ID: 1, Name: "Alice"}, NthOrEmpty(users, 0)) + is.Zero(NthOrEmpty(users, -1)) + is.Zero(NthOrEmpty(users, 10)) + }) +} + +func TestSample(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Sample(values("a", "b", "c")) + result2 := Sample(values[string]()) + + is.True(Contains(values("a", "b", "c"), result1)) + is.Empty(result2) +} + +func TestSampleBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := SampleBy(values("a", "b", "c"), rand.IntN) + result2 := SampleBy(values[string](), rand.IntN) + + is.True(Contains(values("a", "b", "c"), result1)) + is.Empty(result2) +} + +func TestSamples(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Samples(values("a", "b", "c"), 3) + result2 := Samples(values[string](), 3) + + is.ElementsMatch(slices.Collect(result1), []string{"a", "b", "c"}) + is.Empty(slices.Collect(result2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Samples(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestSamplesBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := SamplesBy(values("a", "b", "c"), 3, rand.IntN) + result2 := SamplesBy(values[string](), 3, rand.IntN) + + is.ElementsMatch(slices.Collect(result1), []string{"a", "b", "c"}) + is.Empty(slices.Collect(result2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := SamplesBy(allStrings, 2, rand.IntN) + is.IsType(nonempty, allStrings, "type preserved") +} diff --git a/it/intersect.go b/it/intersect.go new file mode 100644 index 0000000..4d07827 --- /dev/null +++ b/it/intersect.go @@ -0,0 +1,206 @@ +//go:build go1.23 + +package it + +import ( + "iter" + + "github.com/samber/lo" +) + +// Contains returns true if an element is present in a collection. +// Will iterate through the entire sequence if element is not found. +func Contains[T comparable](collection iter.Seq[T], element T) bool { + return ContainsBy(collection, func(item T) bool { return item == element }) +} + +// ContainsBy returns true if predicate function return true. +// Will iterate through the entire sequence if predicate never returns true. +func ContainsBy[T any](collection iter.Seq[T], predicate func(item T) bool) bool { + return IsNotEmpty(Filter(collection, predicate)) +} + +// Every returns true if all elements of a subset are contained in a collection or if the subset is empty. +// Will iterate through the entire sequence if subset elements always match. +func Every[T comparable](collection iter.Seq[T], subset ...T) bool { + if len(subset) == 0 { + return true + } + + set := lo.Keyify(subset) + for item := range collection { + if _, ok := set[item]; ok { + delete(set, item) + if len(set) == 0 { + return true + } + } + } + + return false +} + +// EveryBy returns true if the predicate returns true for all elements in the collection or if the collection is empty. +// Will iterate through the entire sequence if predicate never returns false. +func EveryBy[T any](collection iter.Seq[T], predicate func(item T) bool) bool { + return IsEmpty(Reject(collection, predicate)) +} + +// Some returns true if at least 1 element of a subset is contained in a collection. +// If the subset is empty Some returns false. +// Will iterate through the entire sequence if subset elements never match. +func Some[T comparable](collection iter.Seq[T], subset ...T) bool { + return SomeBy(collection, func(item T) bool { return lo.Contains(subset, item) }) +} + +// SomeBy returns true if the predicate returns true for any of the elements in the collection. +// If the collection is empty SomeBy returns false. +// Will iterate through the entire sequence if predicate never returns true. +func SomeBy[T any](collection iter.Seq[T], predicate func(item T) bool) bool { + return IsNotEmpty(Filter(collection, predicate)) +} + +// None returns true if no element of a subset is contained in a collection or if the subset is empty. +// Will iterate through the entire sequence if subset elements never match. +func None[T comparable](collection iter.Seq[T], subset ...T) bool { + return NoneBy(collection, func(item T) bool { return lo.Contains(subset, item) }) +} + +// NoneBy returns true if the predicate returns true for none of the elements in the collection or if the collection is empty. +// Will iterate through the entire sequence if predicate never returns true. +func NoneBy[T any](collection iter.Seq[T], predicate func(item T) bool) bool { + return IsEmpty(Filter(collection, predicate)) +} + +// Intersect returns the intersection between given collections. +// Will allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func Intersect[T comparable, I ~func(func(T) bool)](lists ...I) I { //nolint:gocyclo + if len(lists) == 0 { + return I(Empty[T]()) + } + + if len(lists) == 1 { + return lists[0] + } + + return func(yield func(T) bool) { + seen := make(map[T]bool) + + for i := len(lists) - 1; i >= 0; i-- { + if i == len(lists)-1 { + for item := range lists[i] { + seen[item] = true + } + continue + } + + if i == 0 { + for item := range lists[0] { + if _, ok := seen[item]; ok { + if !yield(item) { + return + } + delete(seen, item) + } + } + continue + } + + for k := range seen { + seen[k] = false + } + + for item := range lists[i] { + if _, ok := seen[item]; ok { + seen[item] = true + } + } + + for k, v := range seen { + if !v { + delete(seen, k) + } + } + + if len(seen) == 0 { + return + } + } + } +} + +// Union returns all distinct elements from given collections. +// Will allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func Union[T comparable, I ~func(func(T) bool)](lists ...I) I { + return func(yield func(T) bool) { + seen := make(map[T]struct{}) + + for i := range lists { + for item := range lists[i] { + if _, ok := seen[item]; !ok { + if !yield(item) { + return + } + seen[item] = struct{}{} + } + } + } + } +} + +// Without returns a sequence excluding all given values. +// Will allocate a map large enough to hold all distinct excludes. +func Without[T comparable, I ~func(func(T) bool)](collection I, exclude ...T) I { + return WithoutBy(collection, func(item T) T { return item }, exclude...) +} + +// WithoutBy filters a sequence by excluding elements whose extracted keys match any in the exclude list. +// Returns a sequence containing only the elements whose keys are not in the exclude list. +// Will allocate a map large enough to hold all distinct excludes. +func WithoutBy[T any, K comparable, I ~func(func(T) bool)](collection I, transform func(item T) K, exclude ...K) I { + set := lo.Keyify(exclude) + return Reject(collection, func(item T) bool { return lo.HasKey(set, transform(item)) }) +} + +// WithoutNth returns a sequence excluding the nth value. +// Will allocate a map large enough to hold all distinct nths. +func WithoutNth[T comparable, I ~func(func(T) bool)](collection I, nths ...int) I { + set := lo.Keyify(nths) + return RejectI(collection, func(_ T, index int) bool { return lo.HasKey(set, index) }) +} + +// ElementsMatch returns true if lists contain the same set of elements (including empty set). +// If there are duplicate elements, the number of occurrences in each list should match. +// The order of elements is not checked. +// Will iterate through each sequence before returning and allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func ElementsMatch[T comparable](list1, list2 iter.Seq[T]) bool { + return ElementsMatchBy(list1, list2, func(item T) T { return item }) +} + +// ElementsMatchBy returns true if lists contain the same set of elements' keys (including empty set). +// If there are duplicate keys, the number of occurrences in each list should match. +// The order of elements is not checked. +// Will iterate through each sequence before returning and allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func ElementsMatchBy[T any, K comparable](list1, list2 iter.Seq[T], transform func(item T) K) bool { + counters := make(map[K]int) + + for item := range list1 { + counters[transform(item)]++ + } + + for item := range list2 { + counters[transform(item)]-- + } + + for _, count := range counters { + if count != 0 { + return false + } + } + + return true +} diff --git a/it/intersect_example_test.go b/it/intersect_example_test.go new file mode 100644 index 0000000..369a542 --- /dev/null +++ b/it/intersect_example_test.go @@ -0,0 +1,37 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "slices" +) + +func ExampleWithoutBy() { + type User struct { + ID int + Name string + } + // original users + users := values( + User{ID: 1, Name: "Alice"}, + User{ID: 2, Name: "Bob"}, + User{ID: 3, Name: "Charlie"}, + ) + + // exclude users with IDs 2 and 3 + excludedIDs := []int{2, 3} + + // extract function to get the user ID + extractID := func(user User) int { + return user.ID + } + + // filtering users + filteredUsers := WithoutBy(users, extractID, excludedIDs...) + + // output the filtered users + fmt.Printf("%v", slices.Collect(filteredUsers)) + // Output: + // [{1 Alice}] +} diff --git a/it/intersect_test.go b/it/intersect_test.go new file mode 100644 index 0000000..4ab3fdf --- /dev/null +++ b/it/intersect_test.go @@ -0,0 +1,339 @@ +//go:build go1.23 + +package it + +import ( + "iter" + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContains(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Contains(values(0, 1, 2, 3, 4, 5), 5) + result2 := Contains(values(0, 1, 2, 3, 4, 5), 6) + + is.True(result1) + is.False(result2) +} + +func TestContainsBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type a struct { + A int + B string + } + + a1 := values(a{A: 1, B: "1"}, a{A: 2, B: "2"}, a{A: 3, B: "3"}) + result1 := ContainsBy(a1, func(t a) bool { return t.A == 1 && t.B == "2" }) + result2 := ContainsBy(a1, func(t a) bool { return t.A == 2 && t.B == "2" }) + + a2 := values("aaa", "bbb", "ccc") + result3 := ContainsBy(a2, func(t string) bool { return t == "ccc" }) + result4 := ContainsBy(a2, func(t string) bool { return t == "ddd" }) + + is.False(result1) + is.True(result2) + is.True(result3) + is.False(result4) +} + +func TestEvery(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Every(values(0, 1, 2, 3, 4, 5), 0, 2) + result2 := Every(values(0, 1, 2, 3, 4, 5), 0, 6) + result3 := Every(values(0, 1, 2, 3, 4, 5), -1, 6) + result4 := Every(values(0, 1, 2, 3, 4, 5)) + + is.True(result1) + is.False(result2) + is.False(result3) + is.True(result4) +} + +func TestEveryBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := EveryBy(values(1, 2, 3, 4), func(x int) bool { + return x < 5 + }) + + is.True(result1) + + result2 := EveryBy(values(1, 2, 3, 4), func(x int) bool { + return x < 3 + }) + + is.False(result2) + + result3 := EveryBy(values(1, 2, 3, 4), func(x int) bool { + return x < 0 + }) + + is.False(result3) + + result4 := EveryBy(values[int](), func(x int) bool { + return x < 5 + }) + + is.True(result4) +} + +func TestSome(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Some(values(0, 1, 2, 3, 4, 5), 0, 2) + result2 := Some(values(0, 1, 2, 3, 4, 5), 0, 6) + result3 := Some(values(0, 1, 2, 3, 4, 5), -1, 6) + result4 := Some(values(0, 1, 2, 3, 4, 5)) + + is.True(result1) + is.True(result2) + is.False(result3) + is.False(result4) +} + +func TestSomeBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := SomeBy(values(1, 2, 3, 4), func(x int) bool { + return x < 5 + }) + + is.True(result1) + + result2 := SomeBy(values(1, 2, 3, 4), func(x int) bool { + return x < 3 + }) + + is.True(result2) + + result3 := SomeBy(values(1, 2, 3, 4), func(x int) bool { + return x < 0 + }) + + is.False(result3) + + result4 := SomeBy(values[int](), func(x int) bool { + return x < 5 + }) + + is.False(result4) +} + +func TestNone(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := None(values(0, 1, 2, 3, 4, 5), 0, 2) + result2 := None(values(0, 1, 2, 3, 4, 5), 0, 6) + result3 := None(values(0, 1, 2, 3, 4, 5), -1, 6) + result4 := None(values(0, 1, 2, 3, 4, 5)) + + is.False(result1) + is.False(result2) + is.True(result3) + is.True(result4) +} + +func TestNoneBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := NoneBy(values(1, 2, 3, 4), func(x int) bool { + return x < 5 + }) + + is.False(result1) + + result2 := NoneBy(values(1, 2, 3, 4), func(x int) bool { + return x < 3 + }) + + is.False(result2) + + result3 := NoneBy(values(1, 2, 3, 4), func(x int) bool { + return x < 0 + }) + + is.True(result3) + + result4 := NoneBy(values[int](), func(x int) bool { + return x < 5 + }) + + is.True(result4) +} + +func TestIntersect(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Intersect([]iter.Seq[int]{}...) + result2 := Intersect(values(0, 1, 2, 3, 4, 5)) + result3 := Intersect(values(0, 1, 2, 3, 4, 5), values(0, 6)) + result4 := Intersect(values(0, 1, 2, 3, 4, 5), values(-1, 6)) + result5 := Intersect(values(0, 6, 0), values(0, 1, 2, 3, 4, 5)) + result6 := Intersect(values(0, 1, 2, 3, 4, 5), values(0, 6, 0)) + result7 := Intersect(values(0, 1, 2), values(1, 2, 3), values(2, 3, 4)) + result8 := Intersect(values(0, 1, 2), values(1, 2, 3), values(2, 3, 4), values(3, 4, 5)) + result9 := Intersect(values(0, 1, 2), values(0, 1, 2), values(1, 2, 3), values(2, 3, 4), values(3, 4, 5)) + + is.Empty(slices.Collect(result1)) + is.Equal([]int{0, 1, 2, 3, 4, 5}, slices.Collect(result2)) + is.Equal([]int{0}, slices.Collect(result3)) + is.Empty(slices.Collect(result4)) + is.Equal([]int{0}, slices.Collect(result5)) + is.Equal([]int{0}, slices.Collect(result6)) + is.Equal([]int{2}, slices.Collect(result7)) + is.Empty(slices.Collect(result8)) + is.Empty(slices.Collect(result9)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Intersect(allStrings, allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestUnion(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Union(values(0, 1, 2, 3, 4, 5), values(0, 2, 10)) + result2 := Union(values(0, 1, 2, 3, 4, 5), values(6, 7)) + result3 := Union(values(0, 1, 2, 3, 4, 5), values[int]()) + result4 := Union(values(0, 1, 2), values(0, 1, 2, 3, 3)) + result5 := Union(values(0, 1, 2), values(0, 1, 2)) + result6 := Union(values[int](), values[int]()) + is.Equal([]int{0, 1, 2, 3, 4, 5, 10}, slices.Collect(result1)) + is.Equal([]int{0, 1, 2, 3, 4, 5, 6, 7}, slices.Collect(result2)) + is.Equal([]int{0, 1, 2, 3, 4, 5}, slices.Collect(result3)) + is.Equal([]int{0, 1, 2, 3}, slices.Collect(result4)) + is.Equal([]int{0, 1, 2}, slices.Collect(result5)) + is.Empty(slices.Collect(result6)) + + result11 := Union(values(0, 1, 2, 3, 4, 5), values(0, 2, 10), values(0, 1, 11)) + result12 := Union(values(0, 1, 2, 3, 4, 5), values(6, 7), values(8, 9)) + result13 := Union(values(0, 1, 2, 3, 4, 5), values[int](), values[int]()) + result14 := Union(values(0, 1, 2), values(0, 1, 2), values(0, 1, 2)) + result15 := Union(values[int](), values[int](), values[int]()) + is.Equal([]int{0, 1, 2, 3, 4, 5, 10, 11}, slices.Collect(result11)) + is.Equal([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, slices.Collect(result12)) + is.Equal([]int{0, 1, 2, 3, 4, 5}, slices.Collect(result13)) + is.Equal([]int{0, 1, 2}, slices.Collect(result14)) + is.Empty(slices.Collect(result15)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Union(allStrings, allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestWithout(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Without(values(0, 2, 10), 0, 1, 2, 3, 4, 5) + result2 := Without(values(0, 7), 0, 1, 2, 3, 4, 5) + result3 := Without(values[int](), 0, 1, 2, 3, 4, 5) + result4 := Without(values(0, 1, 2), 0, 1, 2) + result5 := Without(values[int]()) + is.Equal([]int{10}, slices.Collect(result1)) + is.Equal([]int{7}, slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + is.Empty(slices.Collect(result4)) + is.Empty(slices.Collect(result5)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Without(allStrings, "") + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestWithoutBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + Name string + Age int + } + + result1 := WithoutBy(values(User{Name: "nick"}, User{Name: "peter"}), + func(item User) string { + return item.Name + }, "nick", "lily") + result2 := WithoutBy(values[User](), func(item User) int { return item.Age }, 1, 2, 3) + result3 := WithoutBy(values[User](), func(item User) string { return item.Name }) + is.Equal([]User{{Name: "peter"}}, slices.Collect(result1)) + is.Empty(slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := WithoutBy(allStrings, func(string) string { return "" }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestWithoutNth(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := WithoutNth(values(5, 6, 7), 1, 0) + is.Equal([]int{7}, slices.Collect(result1)) + + result2 := WithoutNth(values(1, 2)) + is.Equal([]int{1, 2}, slices.Collect(result2)) + + result3 := WithoutNth(values[int]()) + is.Empty(slices.Collect(result3)) + + result4 := WithoutNth(values(0, 1, 2, 3), -1, 4) + is.Equal([]int{0, 1, 2, 3}, slices.Collect(result4)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := WithoutNth(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestElementsMatch(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.False(ElementsMatch(values[int](), values(1))) + is.False(ElementsMatch(values(1), values(2))) + is.False(ElementsMatch(values(1), values(1, 2))) + is.False(ElementsMatch(values(1, 1, 2), values(2, 2, 1))) + + is.True(ElementsMatch(values(1), values(1))) + is.True(ElementsMatch(values(1, 1), values(1, 1))) + is.True(ElementsMatch(values(1, 2), values(2, 1))) + is.True(ElementsMatch(values(1, 1, 2), values(1, 2, 1))) +} + +func TestElementsMatchBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type someType struct { + key string + } + + is.True(ElementsMatchBy( + values(someType{key: "a"}, someType{key: "b"}), + values(someType{key: "b"}, someType{key: "a"}), + func(item someType) string { return item.key }, + )) +} diff --git a/it/lo_test.go b/it/lo_test.go new file mode 100644 index 0000000..fdd91a5 --- /dev/null +++ b/it/lo_test.go @@ -0,0 +1,18 @@ +//go:build go1.23 + +package it + +import ( + "iter" + "slices" +) + +func values[T any](v ...T) iter.Seq[T] { return slices.Values(v) } + +type foo struct { + bar string +} + +func (f foo) Clone() foo { + return foo{f.bar} +} diff --git a/it/map.go b/it/map.go new file mode 100644 index 0000000..25c6063 --- /dev/null +++ b/it/map.go @@ -0,0 +1,245 @@ +//go:build go1.23 + +package it + +import ( + "iter" + "maps" +) + +// Keys creates a sequence of the map keys. +func Keys[K comparable, V any](in ...map[K]V) iter.Seq[K] { + return func(yield func(K) bool) { + for i := range in { + for k := range in[i] { + if !yield(k) { + return + } + } + } + } +} + +// UniqKeys creates a sequence of unique keys in the map. +// Will allocate a map large enough to hold all distinct input keys. +// Long input sequences with heterogeneous keys can cause excessive memory usage. +func UniqKeys[K comparable, V any](in ...map[K]V) iter.Seq[K] { + return func(yield func(K) bool) { + seen := make(map[K]struct{}) + + for i := range in { + for k := range in[i] { + if _, ok := seen[k]; !ok { + if !yield(k) { + return + } + seen[k] = struct{}{} + } + } + } + } +} + +// Values creates a sequence of the map values. +func Values[K comparable, V any](in ...map[K]V) iter.Seq[V] { + return func(yield func(V) bool) { + for i := range in { + for _, v := range in[i] { + if !yield(v) { + return + } + } + } + } +} + +// UniqValues creates a sequence of unique values in the map. +// Will allocate a map large enough to hold all distinct input values. +// Long input sequences with heterogeneous values can cause excessive memory usage. +func UniqValues[K, V comparable](in ...map[K]V) iter.Seq[V] { + return func(yield func(V) bool) { + seen := make(map[V]struct{}) + + for i := range in { + for _, v := range in[i] { + if _, ok := seen[v]; !ok { + if !yield(v) { + return + } + seen[v] = struct{}{} + } + } + } + } +} + +// Entries transforms a map into a sequence of key/value pairs. +func Entries[K comparable, V any](in ...map[K]V) iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, m := range in { + for k, v := range m { + if !yield(k, v) { + return + } + } + } + } +} + +// ToPairs transforms a map into a sequence of key/value pairs. +// Alias of Entries(). +func ToPairs[K comparable, V any](in ...map[K]V) iter.Seq2[K, V] { + return Entries(in...) +} + +// FromEntries transforms a sequence of key/value pairs into a map. +func FromEntries[K comparable, V any](entries ...iter.Seq2[K, V]) map[K]V { + m := make(map[K]V) + for _, e := range entries { + maps.Insert(m, e) + } + return m +} + +// FromPairs transforms a sequence of key/value pairs into a map. +// Alias of FromEntries(). +func FromPairs[K comparable, V any](entries ...iter.Seq2[K, V]) map[K]V { + return FromEntries(entries...) +} + +// Invert creates a sequence composed of inverted keys and values. +func Invert[K, V comparable](in iter.Seq2[K, V]) iter.Seq2[V, K] { + return func(yield func(V, K) bool) { + for k, v := range in { + if !yield(v, k) { + return + } + } + } +} + +// Assign merges multiple sequences of maps from left to right. +func Assign[K comparable, V any, Map ~map[K]V](maps ...iter.Seq[Map]) Map { + out := make(Map) + + for i := range maps { + for item := range maps[i] { + for k, v := range item { + out[k] = v + } + } + } + + return out +} + +// ChunkEntries splits a map into a sequence of elements in groups of length equal to its size. If the map cannot be split evenly, +// the final chunk will contain the remaining elements. +func ChunkEntries[K comparable, V any](m map[K]V, size int) iter.Seq[map[K]V] { + if size <= 0 { + panic("it.ChunkEntries: size must be greater than 0") + } + + return func(yield func(map[K]V) bool) { + var result map[K]V + for k, v := range m { + if result == nil { + result = make(map[K]V, size) + } + result[k] = v + if len(result) == size { + if !yield(result) { + return + } + result = nil + } + } + if result != nil { + yield(result) + } + } +} + +// MapToSeq transforms a map into a sequence based on specified transform. +func MapToSeq[K comparable, V, R any](in map[K]V, transform func(key K, value V) R) iter.Seq[R] { + return func(yield func(R) bool) { + for k, v := range in { + if !yield(transform(k, v)) { + return + } + } + } +} + +// FilterMapToSeq transforms a map into a sequence based on specified transform. +// The transform returns a value and a boolean. If the boolean is true, the value is added to the result sequence. +// If the boolean is false, the value is not added to the result sequence. +// The order of the keys in the input map is not specified and the order of the keys in the output sequence is not guaranteed. +func FilterMapToSeq[K comparable, V, R any](in map[K]V, transform func(key K, value V) (R, bool)) iter.Seq[R] { + return func(yield func(R) bool) { + for k, v := range in { + if v, ok := transform(k, v); ok && !yield(v) { + return + } + } + } +} + +// FilterKeys transforms a map into a sequence based on predicate returns true for specific elements. +// It is a mix of Filter and Keys. +func FilterKeys[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) iter.Seq[K] { + return func(yield func(K) bool) { + for k, v := range in { + if predicate(k, v) && !yield(k) { + return + } + } + } +} + +// FilterValues transforms a map into a sequence based on predicate returns true for specific elements. +// It is a mix of Filter and Values. +func FilterValues[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) iter.Seq[V] { + return func(yield func(V) bool) { + for k, v := range in { + if predicate(k, v) && !yield(v) { + return + } + } + } +} + +// SeqToSeq2 converts a sequence into a sequence of key-value pairs keyed by index. +func SeqToSeq2[T any](in iter.Seq[T]) iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + var i int + for item := range in { + if !yield(i, item) { + return + } + i++ + } + } +} + +// Seq2KeyToSeq converts a sequence of key-value pairs into a sequence of keys. +func Seq2KeyToSeq[K, V any](in iter.Seq2[K, V]) iter.Seq[K] { + return func(yield func(K) bool) { + for k := range in { + if !yield(k) { + return + } + } + } +} + +// Seq2ValueToSeq converts a sequence of key-value pairs into a sequence of values. +func Seq2ValueToSeq[K, V any](in iter.Seq2[K, V]) iter.Seq[V] { + return func(yield func(V) bool) { + for _, v := range in { + if !yield(v) { + return + } + } + } +} diff --git a/it/map_example_test.go b/it/map_example_test.go new file mode 100644 index 0000000..0240a22 --- /dev/null +++ b/it/map_example_test.go @@ -0,0 +1,188 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "maps" + "slices" + "sort" +) + +func ExampleKeys() { + kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"baz": 3} + + result := slices.Collect(Keys(kv, kv2)) + sort.Strings(result) + fmt.Printf("%v", result) + // Output: [bar baz foo] +} + +func ExampleUniqKeys() { + kv := map[string]int{"foo": 1, "bar": 2} + kv2 := map[string]int{"bar": 3} + + result := slices.Collect(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 := slices.Collect(Values(kv, kv2)) + + 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 := slices.Collect(UniqValues(kv, kv2)) + + sort.Ints(result) + fmt.Printf("%v", result) + // Output: [1 2] +} + +func ExampleEntries() { + kv := map[string]int{"foo": 1, "bar": 2, "baz": 3} + + result := maps.Collect(Entries(kv)) + + fmt.Printf("%v %v %v %v", len(result), result["foo"], result["bar"], result["baz"]) + // Output: 3 1 2 3 +} + +func ExampleFromEntries() { + result := FromEntries(maps.All(map[string]int{ + "foo": 1, + "bar": 2, + "baz": 3, + })) + + fmt.Printf("%v %v %v %v", len(result), result["foo"], result["bar"], result["baz"]) + // Output: 3 1 2 3 +} + +func ExampleInvert() { + kv := maps.All(map[string]int{"foo": 1, "bar": 2, "baz": 3}) + + result := maps.Collect(Invert(kv)) + + fmt.Printf("%v %v %v %v", len(result), result[1], result[2], result[3]) + // Output: 3 foo bar baz +} + +func ExampleAssign() { + result := Assign(values( + map[string]int{"a": 1, "b": 2}, + map[string]int{"b": 3, "c": 4}, + )) + + fmt.Printf("%v %v %v %v", len(result), result["a"], result["b"], result["c"]) + // Output: 3 1 3 4 +} + +func ExampleChunkEntries() { + result := ChunkEntries( + map[string]int{ + "a": 1, + "b": 2, + "c": 3, + "d": 4, + "e": 5, + }, + 3, + ) + + for r := range result { + fmt.Printf("%d\n", len(r)) + } + // Output: + // 3 + // 2 +} + +func ExampleMapToSeq() { + kv := map[int]int64{1: 1, 2: 2, 3: 3, 4: 4} + + result := slices.Collect(MapToSeq(kv, func(k int, v int64) string { + return fmt.Sprintf("%d_%d", k, v) + })) + + sort.Strings(result) + fmt.Printf("%v", result) + // Output: [1_1 2_2 3_3 4_4] +} + +func ExampleFilterMapToSeq() { + kv := map[int]int64{1: 1, 2: 2, 3: 3, 4: 4} + + result := slices.Collect(FilterMapToSeq(kv, func(k int, v int64) (string, bool) { + return fmt.Sprintf("%d_%d", k, v), k%2 == 0 + })) + + sort.Strings(result) + fmt.Printf("%v", result) + // Output: [2_2 4_4] +} + +func ExampleFilterKeys() { + kv := map[int]string{1: "foo", 2: "bar", 3: "baz"} + + result := slices.Collect(FilterKeys(kv, func(k int, v string) bool { + return v == "foo" + })) + + fmt.Printf("%v", result) + // Output: [1] +} + +func ExampleFilterValues() { + kv := map[int]string{1: "foo", 2: "bar", 3: "baz"} + + result := slices.Collect(FilterValues(kv, func(k int, v string) bool { + return v == "foo" + })) + + fmt.Printf("%v", result) + // Output: [foo] +} + +func ExampleSeqToSeq2() { + result := maps.Collect(SeqToSeq2(slices.Values([]string{"foo", "bar", "baz"}))) + + fmt.Printf("%v %v %v %v", len(result), result[0], result[1], result[2]) + // Output: 3 foo bar baz +} + +func ExampleSeq2KeyToSeq() { + result := slices.Collect(Seq2KeyToSeq(maps.All(map[string]int{ + "foo": 1, + "bar": 2, + "baz": 3, + }))) + + sort.Strings(result) + fmt.Printf("%v", result) + // Output: [bar baz foo] +} + +func ExampleSeq2ValueToSeq() { + result := slices.Collect(Seq2ValueToSeq(maps.All(map[string]int{ + "foo": 1, + "bar": 2, + "baz": 3, + }))) + + sort.Ints(result) + fmt.Printf("%v", result) + // Output: [1 2 3] +} diff --git a/it/map_test.go b/it/map_test.go new file mode 100644 index 0000000..8b7990d --- /dev/null +++ b/it/map_test.go @@ -0,0 +1,364 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "maps" + "slices" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKeys(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(Keys(map[string]int{"foo": 1, "bar": 2})) + is.ElementsMatch(r1, []string{"bar", "foo"}) + + r2 := slices.Collect(Keys(map[string]int{})) + is.Empty(r2) + + r3 := slices.Collect(Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3})) + is.ElementsMatch(r3, []string{"bar", "baz", "foo"}) + + r4 := slices.Collect(Keys[string, int]()) + is.Empty(r4) + + r5 := slices.Collect(Keys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"bar": 3})) + is.ElementsMatch(r5, []string{"bar", "bar", "foo"}) +} + +func TestUniqKeys(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(UniqKeys(map[string]int{"foo": 1, "bar": 2})) + is.ElementsMatch(r1, []string{"bar", "foo"}) + + r2 := slices.Collect(UniqKeys(map[string]int{})) + is.Empty(r2) + + r3 := slices.Collect(UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3})) + is.ElementsMatch(r3, []string{"bar", "baz", "foo"}) + + r4 := slices.Collect(UniqKeys[string, int]()) + is.Empty(r4) + + r5 := slices.Collect(UniqKeys(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3})) + is.ElementsMatch(r5, []string{"bar", "foo"}) + + // check order + r6 := slices.Collect(UniqKeys(map[string]int{"foo": 1}, map[string]int{"bar": 3})) + is.Equal([]string{"foo", "bar"}, r6) +} + +func TestValues(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(Values(map[string]int{"foo": 1, "bar": 2})) + is.ElementsMatch(r1, []int{1, 2}) + + r2 := slices.Collect(Values(map[string]int{})) + is.Empty(r2) + + r3 := slices.Collect(Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3})) + is.ElementsMatch(r3, []int{1, 2, 3}) + + r4 := slices.Collect(Values[string, int]()) + is.Empty(r4) + + r5 := slices.Collect(Values(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3})) + is.ElementsMatch(r5, []int{1, 1, 2, 3}) +} + +func TestUniqValues(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(UniqValues(map[string]int{"foo": 1, "bar": 2})) + is.ElementsMatch(r1, []int{1, 2}) + + r2 := slices.Collect(UniqValues(map[string]int{})) + is.Empty(r2) + + r3 := slices.Collect(UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"baz": 3})) + is.ElementsMatch(r3, []int{1, 2, 3}) + + r4 := slices.Collect(UniqValues[string, int]()) + is.Empty(r4) + + r5 := slices.Collect(UniqValues(map[string]int{"foo": 1, "bar": 2}, map[string]int{"foo": 1, "bar": 3})) + is.ElementsMatch(r5, []int{1, 2, 3}) + + r6 := slices.Collect(UniqValues(map[string]int{"foo": 1, "bar": 1}, map[string]int{"foo": 1, "bar": 3})) + is.ElementsMatch(r6, []int{1, 3}) + + // check order + r7 := slices.Collect(UniqValues(map[string]int{"foo": 1}, map[string]int{"bar": 3})) + is.Equal([]int{1, 3}, r7) +} + +func TestEntries(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := maps.Collect(Entries(map[string]int{"foo": 1, "bar": 2})) + is.Equal(map[string]int{"foo": 1, "bar": 2}, r1) +} + +func TestToPairs(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := maps.Collect(ToPairs(map[string]int{"foo": 1, "bar": 2})) + is.Equal(map[string]int{"foo": 1, "bar": 2}, r1) +} + +func TestFromEntries(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := FromEntries(maps.All(map[string]int{"foo": 1, "bar": 2})) + + is.Len(r1, 2) + is.Equal(1, r1["foo"]) + is.Equal(2, r1["bar"]) +} + +func TestFromPairs(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := FromPairs(maps.All(map[string]int{"baz": 3, "qux": 4})) + + is.Len(r1, 2) + is.Equal(3, r1["baz"]) + is.Equal(4, r1["qux"]) +} + +func TestInvert(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Invert(maps.All(map[string]int{"a": 1, "b": 2})) + r2 := Invert(maps.All(map[string]int{"a": 1, "b": 2, "c": 1})) + + is.Equal(map[int]string{1: "a", 2: "b"}, maps.Collect(r1)) + is.Len(maps.Collect(r2), 2) +} + +func TestAssign(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Assign(values(map[string]int{"a": 1, "b": 2}, map[string]int{"b": 3, "c": 4})) + is.Equal(map[string]int{"a": 1, "b": 3, "c": 4}, result1) + + type myMap map[string]int + before := myMap{"": 0, "foobar": 6, "baz": 3} + after := Assign(values(before, before)) + is.IsType(myMap{}, after, "type preserved") +} + +func TestChunkEntries(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ChunkEntries(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, 2) + result2 := ChunkEntries(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, 3) + result3 := ChunkEntries(map[string]int{}, 2) + result4 := ChunkEntries(map[string]int{"a": 1}, 2) + result5 := ChunkEntries(map[string]int{"a": 1, "b": 2}, 1) + + is.Len(slices.Collect(result1), 3) + is.Len(slices.Collect(result2), 2) + is.Empty(slices.Collect(result3)) + is.Len(slices.Collect(result4), 1) + is.Len(slices.Collect(result5), 2) + + is.PanicsWithValue("it.ChunkEntries: size must be greater than 0", func() { + ChunkEntries(map[string]int{"a": 1}, 0) + }) + is.PanicsWithValue("it.ChunkEntries: size must be greater than 0", func() { + ChunkEntries(map[string]int{"a": 1}, -1) + }) + + type myStruct struct { + Name string + Value int + } + + allStructs := []myStruct{{"one", 1}, {"two", 2}, {"three", 3}} + nonempty := ChunkEntries(map[string]myStruct{"a": allStructs[0], "b": allStructs[1], "c": allStructs[2]}, 2) + is.Len(slices.Collect(nonempty), 2) + + originalMap := map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5} + result6 := slices.Collect(ChunkEntries(originalMap, 2)) + for k := range result6[0] { + result6[0][k] = 10 + } + is.Equal(map[string]int{"a": 1, "b": 2, "c": 3, "d": 4, "e": 5}, originalMap) +} + +func TestMapToSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := MapToSeq(map[int]int{1: 5, 2: 6, 3: 7, 4: 8}, func(k, v int) string { + return fmt.Sprintf("%d_%d", k, v) + }) + result2 := MapToSeq(map[int]int{1: 5, 2: 6, 3: 7, 4: 8}, func(k, _ int) string { + return strconv.FormatInt(int64(k), 10) + }) + + is.ElementsMatch(slices.Collect(result1), []string{"1_5", "2_6", "3_7", "4_8"}) + is.ElementsMatch(slices.Collect(result2), []string{"1", "2", "3", "4"}) +} + +func TestFilterMapToSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FilterMapToSeq(map[int]int{1: 5, 2: 6, 3: 7, 4: 8}, func(k, v int) (string, bool) { + return fmt.Sprintf("%d_%d", k, v), k%2 == 0 + }) + result2 := FilterMapToSeq(map[int]int{1: 5, 2: 6, 3: 7, 4: 8}, func(k, _ int) (string, bool) { + return strconv.FormatInt(int64(k), 10), k%2 == 0 + }) + + is.ElementsMatch(slices.Collect(result1), []string{"2_6", "4_8"}) + is.ElementsMatch(slices.Collect(result2), []string{"2", "4"}) +} + +func TestFilterKeys(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FilterKeys(map[int]string{1: "foo", 2: "bar", 3: "baz"}, func(k int, v string) bool { + return v == "foo" + }) + is.Equal([]int{1}, slices.Collect(result1)) + + result2 := FilterKeys(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false + }) + is.Empty(slices.Collect(result2)) +} + +func TestFilterValues(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FilterValues(map[int]string{1: "foo", 2: "bar", 3: "baz"}, func(k int, v string) bool { + return v == "foo" + }) + is.Equal([]string{"foo"}, slices.Collect(result1)) + + result2 := FilterValues(map[string]int{"foo": 1, "bar": 2, "baz": 3}, func(k string, v int) bool { + return false + }) + is.Empty(slices.Collect(result2)) +} + +func TestSeqToSeq2(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := maps.Collect(SeqToSeq2(values("foo", "bar"))) + is.Equal(map[int]string{0: "foo", 1: "bar"}, r1) + + r2 := maps.Collect(SeqToSeq2(values[string]())) + is.Empty(r2) +} + +func TestSeq2KeyToSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(Seq2KeyToSeq(maps.All(map[string]int{"foo": 4, "bar": 5}))) + is.ElementsMatch([]string{"foo", "bar"}, r1) + + r2 := slices.Collect(Seq2KeyToSeq(maps.All(map[string]int{}))) + is.Empty(r2) +} + +func TestSeq2ValueToSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := slices.Collect(Seq2ValueToSeq(maps.All(map[string]int{"foo": 4, "bar": 5}))) + is.ElementsMatch([]int{4, 5}, r1) + + r2 := slices.Collect(Seq2ValueToSeq(maps.All(map[string]int{}))) + is.Empty(r2) +} + +func BenchmarkAssign(b *testing.B) { + counts := []int{32768, 1024, 128, 32, 2} + + allDifferentMap := func(b *testing.B, n int) []map[string]int { + b.Helper() + defer b.ResetTimer() + m := make([]map[string]int, 0, n) + 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 { + b.Helper() + defer b.ResetTimer() + m := make([]map[string]int, 0, n) + 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(strconv.Itoa(count), func(b *testing.B) { + testCases := []struct { + name string + in []map[string]int + }{ + {"different", differentMap}, + {"same", sameMap}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + b.ResetTimer() + for n := 0; n < b.N; n++ { + result := Assign(values(tc.in...)) + _ = result + } + }) + } + }) + } +} diff --git a/it/math.go b/it/math.go new file mode 100644 index 0000000..b1b2239 --- /dev/null +++ b/it/math.go @@ -0,0 +1,142 @@ +//go:build go1.23 + +package it + +import ( + "iter" + + "github.com/samber/lo" + "github.com/samber/lo/internal/constraints" +) + +// Range creates a sequence of numbers (positive and/or negative) with given length. +func Range(elementNum int) iter.Seq[int] { + length := lo.If(elementNum < 0, -elementNum).Else(elementNum) + step := lo.If(elementNum < 0, -1).Else(1) + return func(yield func(int) bool) { + for i, j := 0, 0; i < length; i, j = i+1, j+step { + if !yield(j) { + return + } + } + } +} + +// RangeFrom creates a sequence of numbers from start with specified length. +func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) iter.Seq[T] { + length := lo.If(elementNum < 0, -elementNum).Else(elementNum) + step := lo.If(elementNum < 0, -1).Else(1) + return func(yield func(T) bool) { + for i, j := 0, start; i < length; i, j = i+1, j+T(step) { + if !yield(j) { + return + } + } + } +} + +// RangeWithSteps creates a sequence of numbers (positive and/or negative) progressing from start up to, but not including end. +// step set to zero will return an empty sequence. +func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) iter.Seq[T] { + return func(yield func(T) bool) { + if start == end || step == 0 { + return + } + if start < end { + if step < 0 { + return + } + for i := start; i < end; i += step { + if !yield(i) { + return + } + } + } + if step > 0 { + return + } + for i := start; i > end; i += step { + if !yield(i) { + return + } + } + } +} + +// Sum sums the values in a collection. If collection is empty 0 is returned. +// Will iterate through the entire sequence. +func Sum[T constraints.Float | constraints.Integer | constraints.Complex](collection iter.Seq[T]) T { + return SumBy(collection, func(item T) T { return item }) +} + +// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned. +// Will iterate through the entire sequence. +func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection iter.Seq[T], transform func(item T) R) R { + var sum R + for item := range collection { + sum += transform(item) + } + return sum +} + +// Product gets the product of the values in a collection. If collection is empty 1 is returned. +// Will iterate through the entire sequence. +func Product[T constraints.Float | constraints.Integer | constraints.Complex](collection iter.Seq[T]) T { + return ProductBy(collection, func(item T) T { return item }) +} + +// ProductBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 1 is returned. +// Will iterate through the entire sequence. +func ProductBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection iter.Seq[T], transform func(item T) R) R { + var product R = 1 + for item := range collection { + product *= transform(item) + } + return product +} + +// Mean calculates the mean of a collection of numbers. +// Will iterate through the entire sequence. +func Mean[T constraints.Float | constraints.Integer](collection iter.Seq[T]) T { + return MeanBy(collection, func(item T) T { return item }) +} + +// MeanBy calculates the mean of a collection of numbers using the given return value from the iteration function. +// Will iterate through the entire sequence. +func MeanBy[T any, R constraints.Float | constraints.Integer](collection iter.Seq[T], transform func(item T) R) R { + var sum R + var length R + for item := range collection { + sum += transform(item) + length++ + } + if length == 0 { + return 0 + } + return sum / length +} + +// Mode returns the mode (most frequent value) of a collection. +// If multiple values have the same highest frequency, then multiple values are returned. +// If the collection is empty, then the zero value of T is returned. +// Will iterate through the entire sequence and allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func Mode[T constraints.Integer | constraints.Float](collection iter.Seq[T]) []T { + var mode []T + maxFreq := 0 + frequency := make(map[T]int) + + for item := range collection { + frequency[item]++ + count := frequency[item] + + if count > maxFreq { + maxFreq = count + mode = append(mode[:0], item) + } else if count == maxFreq { + mode = append(mode, item) + } + } + + return mode +} diff --git a/it/math_example_test.go b/it/math_example_test.go new file mode 100644 index 0000000..6243a03 --- /dev/null +++ b/it/math_example_test.go @@ -0,0 +1,97 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "slices" +) + +func ExampleRange() { + result1 := Range(4) + result2 := Range(-4) + result3 := RangeFrom(1, 5) + result4 := RangeFrom(1.0, 5) + result5 := RangeWithSteps(0, 20, 5) + result6 := RangeWithSteps[float32](-1.0, -4.0, -1.0) + result7 := RangeWithSteps(1, 4, -1) + result8 := Range(0) + + fmt.Printf("%v\n", slices.Collect(result1)) + fmt.Printf("%v\n", slices.Collect(result2)) + fmt.Printf("%v\n", slices.Collect(result3)) + fmt.Printf("%v\n", slices.Collect(result4)) + fmt.Printf("%v\n", slices.Collect(result5)) + fmt.Printf("%v\n", slices.Collect(result6)) + fmt.Printf("%v\n", slices.Collect(result7)) + fmt.Printf("%v\n", slices.Collect(result8)) + // Output: + // [0 1 2 3] + // [0 -1 -2 -3] + // [1 2 3 4 5] + // [1 2 3 4 5] + // [0 5 10 15] + // [-1 -2 -3] + // [] + // [] +} + +func ExampleSum() { + ints := slices.Values([]int{1, 2, 3, 4, 5}) + + sum := Sum(ints) + + fmt.Printf("%v", sum) + // Output: 15 +} + +func ExampleSumBy() { + ints := slices.Values([]string{"foo", "bar"}) + + result := SumBy(ints, func(item string) int { + return len(item) + }) + + fmt.Printf("%v", result) + // Output: 6 +} + +func ExampleProduct() { + ints := slices.Values([]int{1, 2, 3, 4, 5}) + + result := Product(ints) + + fmt.Printf("%v", result) + // Output: 120 +} + +func ExampleProductBy() { + strs := slices.Values([]string{"foo", "bar"}) + + result := ProductBy(strs, func(item string) int { + return len(item) + }) + + fmt.Printf("%v", result) + // Output: 9 +} + +func ExampleMean() { + ints := slices.Values([]int{1, 2, 3, 4, 5}) + + result := Mean(ints) + + fmt.Printf("%v", result) + // Output: 3 +} + +func ExampleMeanBy() { + strs := slices.Values([]string{"foo", "bar"}) + + result := MeanBy(strs, func(item string) int { + return len(item) + }) + + fmt.Printf("%v", result) + // Output: 3 +} diff --git a/it/math_test.go b/it/math_test.go new file mode 100644 index 0000000..dda361a --- /dev/null +++ b/it/math_test.go @@ -0,0 +1,179 @@ +//go:build go1.23 + +package it + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRange(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Range(4) + result2 := Range(-4) + result3 := Range(0) + is.Equal([]int{0, 1, 2, 3}, slices.Collect(result1)) + is.Equal([]int{0, -1, -2, -3}, slices.Collect(result2)) + is.Empty(slices.Collect(result3)) +} + +func TestRangeFrom(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := RangeFrom(1, 5) + result2 := RangeFrom(-1, -5) + result3 := RangeFrom(10, 0) + result4 := RangeFrom(2.0, 3) + result5 := RangeFrom(-2.0, -3) + is.Equal([]int{1, 2, 3, 4, 5}, slices.Collect(result1)) + is.Equal([]int{-1, -2, -3, -4, -5}, slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + is.Equal([]float64{2.0, 3.0, 4.0}, slices.Collect(result4)) + is.Equal([]float64{-2.0, -3.0, -4.0}, slices.Collect(result5)) +} + +func TestRangeClose(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := RangeWithSteps(0, 20, 6) + result2 := RangeWithSteps(0, 3, -5) + result3 := RangeWithSteps(1, 1, 0) + result4 := RangeWithSteps(3, 2, 1) + result5 := RangeWithSteps(1.0, 4.0, 2.0) + result6 := RangeWithSteps[float32](-1.0, -4.0, -1.0) + is.Equal([]int{0, 6, 12, 18}, slices.Collect(result1)) + is.Empty(slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + is.Empty(slices.Collect(result4)) + is.Equal([]float64{1.0, 3.0}, slices.Collect(result5)) + is.Equal([]float32{-1.0, -2.0, -3.0}, slices.Collect(result6)) +} + +func TestSum(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Sum(values[float32](2.3, 3.3, 4, 5.3)) + result2 := Sum(values[int32](2, 3, 4, 5)) + result3 := Sum(values[uint32](2, 3, 4, 5)) + result4 := Sum(values[uint32]()) + result5 := Sum(values[complex128](4_4, 2_2)) + + is.InEpsilon(14.9, result1, 1e-7) + is.Equal(int32(14), result2) + is.Equal(uint32(14), result3) + is.Equal(uint32(0), result4) + is.Equal(complex128(6_6), result5) +} + +func TestSumBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := SumBy(values[float32](2.3, 3.3, 4, 5.3), func(n float32) float32 { return n }) + result2 := SumBy(values[int32](2, 3, 4, 5), func(n int32) int32 { return n }) + result3 := SumBy(values[uint32](2, 3, 4, 5), func(n uint32) uint32 { return n }) + result4 := SumBy(values[uint32](), func(n uint32) uint32 { return n }) + result5 := SumBy(values[complex128](4_4, 2_2), func(n complex128) complex128 { return n }) + + is.InEpsilon(14.9, result1, 1e-7) + is.Equal(int32(14), result2) + is.Equal(uint32(14), result3) + is.Equal(uint32(0), result4) + is.Equal(complex128(6_6), result5) +} + +func TestProduct(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Product(values[float32](2.3, 3.3, 4, 5.3)) + result2 := Product(values[int32](2, 3, 4, 5)) + result3 := Product(values[int32](7, 8, 9, 0)) + result4 := Product(values[int32](7, -1, 9, 2)) + result5 := Product(values[uint32](2, 3, 4, 5)) + result6 := Product(values[uint32]()) + result7 := Product(values[complex128](4_4, 2_2)) + + is.InEpsilon(160.908, result1, 1e-7) + is.Equal(int32(120), result2) + is.Equal(int32(0), result3) + is.Equal(int32(-126), result4) + is.Equal(uint32(120), result5) + is.Equal(uint32(1), result6) + is.Equal(complex128(96_8), result7) +} + +func TestProductBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ProductBy(values[float32](2.3, 3.3, 4, 5.3), func(n float32) float32 { return n }) + result2 := ProductBy(values[int32](2, 3, 4, 5), func(n int32) int32 { return n }) + result3 := ProductBy(values[int32](7, 8, 9, 0), func(n int32) int32 { return n }) + result4 := ProductBy(values[int32](7, -1, 9, 2), func(n int32) int32 { return n }) + result5 := ProductBy(values[uint32](2, 3, 4, 5), func(n uint32) uint32 { return n }) + result6 := ProductBy(values[uint32](), func(n uint32) uint32 { return n }) + result7 := ProductBy(values[complex128](4_4, 2_2), func(n complex128) complex128 { return n }) + + is.InEpsilon(160.908, result1, 1e-7) + is.Equal(int32(120), result2) + is.Equal(int32(0), result3) + is.Equal(int32(-126), result4) + is.Equal(uint32(120), result5) + is.Equal(uint32(1), result6) + is.Equal(complex128(96_8), result7) +} + +func TestMean(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Mean(values[float32](2.3, 3.3, 4, 5.3)) + result2 := Mean(values[int32](2, 3, 4, 5)) + result3 := Mean(values[uint32](2, 3, 4, 5)) + result4 := Mean(values[uint32]()) + + is.InEpsilon(3.725, result1, 1e-7) + is.Equal(int32(3), result2) + is.Equal(uint32(3), result3) + is.Equal(uint32(0), result4) +} + +func TestMeanBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := MeanBy(values[float32](2.3, 3.3, 4, 5.3), func(n float32) float32 { return n }) + result2 := MeanBy(values[int32](2, 3, 4, 5), func(n int32) int32 { return n }) + result3 := MeanBy(values[uint32](2, 3, 4, 5), func(n uint32) uint32 { return n }) + result4 := MeanBy(values[uint32](), func(n uint32) uint32 { return n }) + + is.InEpsilon(3.725, result1, 1e-7) + is.Equal(int32(3), result2) + is.Equal(uint32(3), result3) + is.Equal(uint32(0), result4) +} + +func TestMode(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Mode(values[float32](2.3, 3.3, 3.3, 5.3)) + result2 := Mode(values[int32](2, 2, 3, 4)) + result3 := Mode(values[uint32](2, 2, 3, 3)) + result4 := Mode(values[uint32]()) + result5 := Mode(values(1, 2, 3, 4, 5, 6, 7, 8, 9)) + + is.Equal([]float32{3.3}, result1) + is.Equal([]int32{2}, result2) + is.Equal([]uint32{2, 3}, result3) + is.Empty(result4) + is.Equal([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}, result5) +} diff --git a/it/seq.go b/it/seq.go new file mode 100644 index 0000000..f433a7b --- /dev/null +++ b/it/seq.go @@ -0,0 +1,904 @@ +//go:build go1.23 + +package it + +import ( + "iter" + "slices" + + "github.com/samber/lo" + "github.com/samber/lo/internal/constraints" + "github.com/samber/lo/mutable" +) + +// Length returns the length of collection. +// Will iterate through the entire sequence. +func Length[T any](collection iter.Seq[T]) int { + var count int + + for range collection { + count++ + } + + return count +} + +// Drain consumes an entire sequence. +func Drain[T any](collection iter.Seq[T]) { + for range collection { //nolint:revive + } +} + +// Filter iterates over elements of collection, returning a sequence of all elements predicate returns true for. +func Filter[T any, I ~func(func(T) bool)](collection I, predicate func(item T) bool) I { + return FilterI(collection, func(item T, _ int) bool { return predicate(item) }) +} + +// FilterI iterates over elements of collection, returning a sequence of all elements predicate returns true for. +func FilterI[T any, I ~func(func(T) bool)](collection I, predicate func(item T, index int) bool) I { + return func(yield func(T) bool) { + var i int + for item := range collection { + if predicate(item, i) && !yield(item) { + return + } + i++ + } + } +} + +// Map manipulates a sequence and transforms it to a sequence of another type. +func Map[T, R any](collection iter.Seq[T], transform func(item T) R) iter.Seq[R] { + return MapI(collection, func(item T, _ int) R { return transform(item) }) +} + +// MapI manipulates a sequence and transforms it to a sequence of another type. +func MapI[T, R any](collection iter.Seq[T], transform func(item T, index int) R) iter.Seq[R] { + return func(yield func(R) bool) { + var i int + for item := range collection { + if !yield(transform(item, i)) { + return + } + i++ + } + } +} + +// UniqMap manipulates a sequence and transforms it to a sequence of another type with unique values. +// Will allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func UniqMap[T any, R comparable](collection iter.Seq[T], transform func(item T) R) iter.Seq[R] { + return Uniq(Map(collection, transform)) +} + +// UniqMapI manipulates a sequence and transforms it to a sequence of another type with unique values. +// Will allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func UniqMapI[T any, R comparable](collection iter.Seq[T], transform func(item T, index int) R) iter.Seq[R] { + return Uniq(MapI(collection, transform)) +} + +// FilterMap returns a sequence obtained after both filtering and mapping using the given callback function. +// The callback function should return two values: +// - the result of the mapping operation and +// - whether the result element should be included or not. +func FilterMap[T, R any](collection iter.Seq[T], callback func(item T) (R, bool)) iter.Seq[R] { + return FilterMapI(collection, func(item T, _ int) (R, bool) { return callback(item) }) +} + +// FilterMapI returns a sequence obtained after both filtering and mapping using the given callback function. +// The callback function should return two values: +// - the result of the mapping operation and +// - whether the result element should be included or not. +func FilterMapI[T, R any](collection iter.Seq[T], callback func(item T, index int) (R, bool)) iter.Seq[R] { + return func(yield func(R) bool) { + var i int + for item := range collection { + if r, ok := callback(item, i); ok && !yield(r) { + return + } + i++ + } + } +} + +// FlatMap manipulates a sequence and transforms and flattens it to a sequence of another type. +// The transform function can either return a sequence or a `nil`, and in the `nil` case +// no value is yielded. +func FlatMap[T, R any](collection iter.Seq[T], transform func(item T) iter.Seq[R]) iter.Seq[R] { + return FlatMapI(collection, func(item T, _ int) iter.Seq[R] { return transform(item) }) +} + +// FlatMapI manipulates a sequence and transforms and flattens it to a sequence of another type. +// The transform function can either return a sequence or a `nil`, and in the `nil` case +// no value is yielded. +func FlatMapI[T, R any](collection iter.Seq[T], transform func(item T, index int) iter.Seq[R]) iter.Seq[R] { + return func(yield func(R) bool) { + var i int + for item := range collection { + for r := range transform(item, i) { + if !yield(r) { + return + } + } + i++ + } + } +} + +// Reduce reduces collection to a value which is the accumulated result of running each element in collection +// through accumulator, where each successive invocation is supplied the return value of the previous. +// Will iterate through the entire sequence. +func Reduce[T, R any](collection iter.Seq[T], accumulator func(agg R, item T) R, initial R) R { + return ReduceI(collection, func(agg R, item T, _ int) R { return accumulator(agg, item) }, initial) +} + +// ReduceI reduces collection to a value which is the accumulated result of running each element in collection +// through accumulator, where each successive invocation is supplied the return value of the previous. +// Will iterate through the entire sequence. +func ReduceI[T, R any](collection iter.Seq[T], accumulator func(agg R, item T, index int) R, initial R) R { + var i int + for item := range collection { + initial = accumulator(initial, item, i) + i++ + } + + return initial +} + +// ReduceLast is like Reduce except that it iterates over elements of collection in reverse. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func ReduceLast[T, R any](collection iter.Seq[T], accumulator func(agg R, item T) R, initial R) R { + return Reduce(Reverse(collection), accumulator, initial) +} + +// ReduceLastI is like Reduce except that it iterates over elements of collection in reverse. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func ReduceLastI[T, R any](collection iter.Seq[T], accumulator func(agg R, item T, index int) R, initial R) R { + return ReduceI(Reverse(collection), accumulator, initial) +} + +// ForEach iterates over elements of collection and invokes transform for each element. +// Will iterate through the entire sequence. +func ForEach[T any](collection iter.Seq[T], transform func(item T)) { + ForEachI(collection, func(item T, _ int) { transform(item) }) +} + +// ForEachI iterates over elements of collection and invokes transform for each element. +// Will iterate through the entire sequence. +func ForEachI[T any](collection iter.Seq[T], transform func(item T, index int)) { + var i int + for item := range collection { + transform(item, i) + i++ + } +} + +// ForEachWhile iterates over elements of collection and invokes predicate for each element +// collection return value decide to continue or break, like do while(). +// Will iterate through the entire sequence. +func ForEachWhile[T any](collection iter.Seq[T], predicate func(item T) bool) { + ForEachWhileI(collection, func(item T, _ int) bool { return predicate(item) }) +} + +// ForEachWhileI iterates over elements of collection and invokes predicate for each element +// collection return value decide to continue or break, like do while(). +// Will iterate through the entire sequence. +func ForEachWhileI[T any](collection iter.Seq[T], predicate func(item T, index int) bool) { + var i int + for item := range collection { + if !predicate(item, i) { + return + } + i++ + } +} + +// Times invokes transform n times and returns a sequence of results. +// The transform is invoked with index as argument. +func Times[T any](count int, transform func(index int) T) iter.Seq[T] { + return func(yield func(T) bool) { + for i := 0; i < count; i++ { + if !yield(transform(i)) { + return + } + } + } +} + +// Uniq returns a duplicate-free version of a sequence, 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 sequence. +// Will allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func Uniq[T comparable, I ~func(func(T) bool)](collection I) I { + return UniqBy(collection, func(item T) T { return item }) +} + +// UniqBy returns a duplicate-free version of a sequence, 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 sequence. A transform function is +// invoked for each element in the sequence to generate the criterion by which uniqueness is computed. +// Will allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func UniqBy[T any, U comparable, I ~func(func(T) bool)](collection I, transform func(item T) U) I { + return func(yield func(T) bool) { + seen := make(map[U]struct{}) + + for item := range collection { + key := transform(item) + + if _, ok := seen[key]; !ok { + if !yield(item) { + return + } + seen[key] = struct{}{} + } + } + } +} + +// GroupBy returns an object composed of keys generated from the results of running each element of collection through transform. +// Will iterate through the entire sequence. +func GroupBy[T any, U comparable](collection iter.Seq[T], transform func(item T) U) map[U][]T { + return GroupByMap(collection, func(item T) (U, T) { return transform(item), item }) +} + +// GroupByMap returns an object composed of keys generated from the results of running each element of collection through transform. +// Will iterate through the entire sequence. +func GroupByMap[T any, K comparable, V any](collection iter.Seq[T], transform func(item T) (K, V)) map[K][]V { + result := make(map[K][]V) + + for item := range collection { + k, v := transform(item) + + result[k] = append(result[k], v) + } + + return result +} + +// Chunk returns a sequence of elements split into groups of length size. If the sequence can't be split evenly, +// the final chunk will be the remaining elements. +func Chunk[T any](collection iter.Seq[T], size int) iter.Seq[[]T] { + if size <= 0 { + panic("it.Chunk: size must be greater than 0") + } + + return func(yield func([]T) bool) { + var newSlice []T + for item := range collection { + if newSlice == nil { + newSlice = make([]T, 0, size) + } + newSlice = append(newSlice, item) + if len(newSlice) == size { + if !yield(newSlice) { + return + } + newSlice = nil + } + } + if newSlice != nil { + yield(newSlice) + } + } +} + +// PartitionBy returns a sequence of elements split into groups. The order of grouped values is +// determined by the order they occur in collection. The grouping is generated from the results +// of running each element of collection through transform. +// Will allocate a map large enough to hold all distinct transformed elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func PartitionBy[T any, K comparable](collection iter.Seq[T], transform func(item T) K) [][]T { + var result [][]T + seen := map[K]int{} + + for item := range collection { + key := transform(item) + + resultIndex, ok := seen[key] + if !ok { + resultIndex = len(result) + seen[key] = resultIndex + result = append(result, []T{}) + } + + result[resultIndex] = append(result[resultIndex], item) + } + + return result +} + +// Flatten returns a sequence a single level deep. +func Flatten[T any, I ~func(func(T) bool)](collection []I) I { + return func(yield func(T) bool) { + for _, item := range collection { + for item := range item { + if !yield(item) { + return + } + } + } + } +} + +// Interleave round-robin alternating input sequences and sequentially appending value at index into result. +// Will allocate a slice the size of collections. +func Interleave[T any](collections ...iter.Seq[T]) iter.Seq[T] { + return func(yield func(T) bool) { + next := make([]func() (T, bool), len(collections)) + for i, c := range collections { + var stop func() + next[i], stop = iter.Pull(c) + defer stop() + } + var done int + for done < len(next) { + done = 0 + for i, n := range next { + if n == nil { + done++ + } else if t, ok := n(); !ok { + next[i] = nil + done++ + } else if !yield(t) { + return + } + } + } + } +} + +// Shuffle returns a sequence of shuffled values. Uses the Fisher-Yates shuffle algorithm. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func Shuffle[T any, I ~func(func(T) bool)](collection I) I { + slice := slices.Collect(iter.Seq[T](collection)) + mutable.Shuffle(slice) + return I(slices.Values(slice)) +} + +// Reverse reverses a sequence so that the first element becomes the last, the second element becomes the second to last, and so on. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func Reverse[T any, I ~func(func(T) bool)](collection I) I { + slice := slices.Collect(iter.Seq[T](collection)) + mutable.Reverse(slice) + return I(slices.Values(slice)) +} + +// Fill replaces elements of a sequence with `initial` value. +func Fill[T lo.Clonable[T], I ~func(func(T) bool)](collection I, initial T) I { + return func(yield func(T) bool) { + for range collection { + if !yield(initial.Clone()) { + return + } + } + } +} + +// Repeat builds a sequence with N copies of initial value. +func Repeat[T lo.Clonable[T]](count int, initial T) iter.Seq[T] { + return RepeatBy(count, func(int) T { return initial.Clone() }) +} + +// RepeatBy builds a sequence with values returned by N calls of transform. +func RepeatBy[T any](count int, transform func(index int) T) iter.Seq[T] { + return func(yield func(T) bool) { + for i := range count { + if !yield(transform(i)) { + return + } + } + } +} + +// KeyBy transforms a sequence to a map based on a pivot transform function. +// Will iterate through the entire sequence. +func KeyBy[K comparable, V any](collection iter.Seq[V], transform func(item V) K) map[K]V { + result := make(map[K]V) + + for item := range collection { + k := transform(item) + result[k] = item + } + + return result +} + +// Associate returns a map containing key-value pairs provided by transform function applied to elements of the given sequence. +// If any of two pairs have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original sequence. +// Will iterate through the entire sequence. +func Associate[T any, K comparable, V any](collection iter.Seq[T], transform func(item T) (K, V)) map[K]V { + result := make(map[K]V) + + for item := range collection { + k, v := transform(item) + result[k] = v + } + + return result +} + +// SeqToMap returns a map containing key-value pairs provided by transform function applied to elements of the given sequence. +// If any of two pairs have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original sequence. +// Alias of Associate(). +// Will iterate through the entire sequence. +func SeqToMap[T any, K comparable, V any](collection iter.Seq[T], transform func(item T) (K, V)) map[K]V { + return Associate(collection, transform) +} + +// FilterSeqToMap returns a map containing key-value pairs provided by transform function applied to elements of the given sequence. +// If any of two pairs have the same key the last one gets added to the map. +// The order of keys in returned map is not specified and is not guaranteed to be the same from the original sequence. +// The third return value of the transform function is a boolean that indicates whether the key-value pair should be included in the map. +// Will iterate through the entire sequence. +func FilterSeqToMap[T any, K comparable, V any](collection iter.Seq[T], transform func(item T) (K, V, bool)) map[K]V { + result := make(map[K]V) + + for item := range collection { + if k, v, ok := transform(item); ok { + result[k] = v + } + } + + return result +} + +// Keyify returns a map with each unique element of the sequence as a key. +// Will iterate through the entire sequence. +func Keyify[T comparable](collection iter.Seq[T]) map[T]struct{} { + result := make(map[T]struct{}) + + for item := range collection { + result[item] = struct{}{} + } + + return result +} + +// Drop drops n elements from the beginning of a sequence. +func Drop[T any, I ~func(func(T) bool)](collection I, n int) I { + if n < 0 { + panic("it.Drop: n must not be negative") + } + + if n == 0 { + return collection + } + + return FilterI(collection, func(item T, index int) bool { return index >= n }) +} + +// DropLast drops n elements from the end of a sequence. +// Will allocate a slice of length n. +func DropLast[T any, I ~func(func(T) bool)](collection I, n int) I { + if n < 0 { + panic("it.DropLast: n must not be negative") + } + + if n == 0 { + return collection + } + + return func(yield func(T) bool) { + buf := make([]T, 0, n) + var i int + for item := range collection { + if len(buf) < n { + buf = append(buf, item) + } else { + if !yield(buf[i]) { + return + } + buf[i] = item + i = (i + 1) % n + } + } + } +} + +// DropWhile drops elements from the beginning of a sequence while the predicate returns true. +func DropWhile[T any, I ~func(func(T) bool)](collection I, predicate func(item T) bool) I { + return func(yield func(T) bool) { + dropping := true + for item := range collection { + if dropping && !predicate(item) { + dropping = false + } + if !dropping && !yield(item) { + return + } + } + } +} + +// DropLastWhile drops elements from the end of a sequence while the predicate returns true. +// Will allocate a slice large enough to hold the longest sequence of matching elements. +// Long input sequences of consecutive matches can cause excessive memory usage. +func DropLastWhile[T any, I ~func(func(T) bool)](collection I, predicate func(item T) bool) I { + return func(yield func(T) bool) { + var buf []T + for item := range collection { + if predicate(item) { + buf = append(buf, item) + continue + } + if len(buf) > 0 { + for _, item := range buf { + if !yield(item) { + return + } + } + buf = buf[:0] + } + if !yield(item) { + return + } + } + } +} + +// DropByIndex drops elements from a sequence by the index. +// Will allocate a map large enough to hold all distinct indexes. +func DropByIndex[T any, I ~func(func(T) bool)](collection I, indexes ...int) I { + set := lo.Keyify(indexes) + return RejectI(collection, func(_ T, index int) bool { return lo.HasKey(set, index) }) +} + +// Reject is the opposite of Filter, this method returns the elements of collection that predicate does not return true for. +func Reject[T any, I ~func(func(T) bool)](collection I, predicate func(item T) bool) I { + return RejectI(collection, func(item T, _ int) bool { return predicate(item) }) +} + +// RejectI is the opposite of Filter, this method returns the elements of collection that predicate does not return true for. +func RejectI[T any, I ~func(func(T) bool)](collection I, predicate func(item T, index int) bool) I { + return func(yield func(T) bool) { + var i int + for item := range collection { + if !predicate(item, i) && !yield(item) { + return + } + i++ + } + } +} + +// RejectMap is the opposite of FilterMap, this method returns a sequence obtained after both filtering and mapping using the given callback function. +// The callback function should return two values: +// - the result of the mapping operation and +// - whether the result element should be included or not. +func RejectMap[T, R any](collection iter.Seq[T], callback func(item T) (R, bool)) iter.Seq[R] { + return RejectMapI(collection, func(item T, _ int) (R, bool) { return callback(item) }) +} + +// RejectMapI is the opposite of FilterMap, this method returns a sequence obtained after both filtering and mapping using the given callback function. +// The callback function should return two values: +// - the result of the mapping operation and +// - whether the result element should be included or not. +func RejectMapI[T, R any](collection iter.Seq[T], callback func(item T, index int) (R, bool)) iter.Seq[R] { + return func(yield func(R) bool) { + var i int + for item := range collection { + if r, ok := callback(item, i); !ok && !yield(r) { + return + } + i++ + } + } +} + +// Count counts the number of elements in the collection that equal value. +// Will iterate through the entire sequence. +func Count[T comparable](collection iter.Seq[T], value T) int { + return CountBy(collection, func(item T) bool { return item == value }) +} + +// CountBy counts the number of elements in the collection for which predicate is true. +// Will iterate through the entire sequence. +func CountBy[T any](collection iter.Seq[T], predicate func(item T) bool) int { + var count int + + for range Filter(collection, predicate) { + count++ + } + + return count +} + +// CountValues counts the number of each element in the collection. +// Will iterate through the entire sequence. +func CountValues[T comparable](collection iter.Seq[T]) map[T]int { + return CountValuesBy(collection, func(item T) T { return item }) +} + +// CountValuesBy counts the number of each element returned from transform function. +// Is equivalent to chaining Map and CountValues. +// Will iterate through the entire sequence. +func CountValuesBy[T any, U comparable](collection iter.Seq[T], transform func(item T) U) map[U]int { + result := make(map[U]int) + + for item := range collection { + result[transform(item)]++ + } + + return result +} + +// Subset returns a subset of a sequence from `offset` up to `length` elements. +// Will iterate at most offset+length times. +func Subset[T any, I ~func(func(T) bool)](collection I, offset, length int) I { + if offset < 0 { + panic("it.Subset: offset must not be negative") + } + if length < 0 { + panic("it.Subset: length must not be negative") + } + + return Slice(collection, offset, offset+length) +} + +// Slice returns a subset of a sequence from `start` up to, but not including `end`. +// Will iterate at most end times. +func Slice[T any, I ~func(func(T) bool)](collection I, start, end int) I { + if start < 0 { + start = 0 + } + if end < 0 { + end = 0 + } + + return func(yield func(T) bool) { + var i int + for item := range collection { + if i >= start && (i >= end || !yield(item)) { + return + } + i++ + } + } +} + +// Replace returns a sequence with the first n non-overlapping instances of old replaced by new. +func Replace[T comparable, I ~func(func(T) bool)](collection I, old, nEw T, n int) I { + return I(Map(iter.Seq[T](collection), func(item T) T { + if n != 0 && item == old { + n-- + return nEw + } + return item + })) +} + +// ReplaceAll returns a sequence with all non-overlapping instances of old replaced by new. +func ReplaceAll[T comparable, I ~func(func(T) bool)](collection I, old, nEw T) I { + return Replace(collection, old, nEw, -1) +} + +// Compact returns a sequence of all non-zero elements. +func Compact[T comparable, I ~func(func(T) bool)](collection I) I { + return Filter(collection, lo.IsNotEmpty) +} + +// IsSorted checks if a sequence is sorted. +// Will iterate through the entire sequence. +func IsSorted[T constraints.Ordered](collection iter.Seq[T]) bool { + return IsSortedBy(collection, func(item T) T { return item }) +} + +// IsSortedBy checks if a sequence is sorted by transform. +// Will iterate through the entire sequence. +func IsSortedBy[T any, K constraints.Ordered](collection iter.Seq[T], transform func(item T) K) bool { + first := true + var prev K + for item := range collection { + key := transform(item) + if first { + first = false + } else if prev > key { + return false + } + prev = key + } + return true +} + +// Splice inserts multiple elements at index i. The helper is protected against overflow errors. +func Splice[T any, I ~func(func(T) bool)](collection I, index int, elements ...T) I { + if index < 0 { + panic("it.Splice: index must not be negative") + } + + if len(elements) == 0 { + return collection + } + + return func(yield func(T) bool) { + var i int + for item := range collection { + if i == index { + for _, element := range elements { + if !yield(element) { + return + } + } + } + if !yield(item) { + return + } + i++ + } + if i <= index { + for _, element := range elements { + if !yield(element) { + return + } + } + } + } +} + +// CutPrefix returns collection without the provided leading prefix +// and reports whether it found the prefix. +// If collection doesn't start with prefix, CutPrefix returns collection, false. +// If prefix is empty, CutPrefix returns collection, true. +// Will iterate at most the size of separator before returning. +func CutPrefix[T comparable, I ~func(func(T) bool)](collection I, separator []T) (after I, found bool) { //nolint:gocyclo + if len(separator) == 0 { + return collection, true + } + + next, stop := iter.Pull(iter.Seq[T](collection)) + for i := range separator { + item, ok := next() + if !ok { + return func(yield func(T) bool) { + defer stop() + for j := 0; j < i; j++ { + if !yield(separator[j]) { + return + } + } + }, false + } + + if item != separator[i] { + return func(yield func(T) bool) { + defer stop() + for j := 0; j < i; j++ { + if !yield(separator[j]) { + return + } + } + if ok && !yield(item) { + return + } + for { + if item, ok := next(); !ok || !yield(item) { + return + } + } + }, false + } + } + + return func(yield func(T) bool) { + defer stop() + for { + if item, ok := next(); !ok || !yield(item) { + return + } + } + }, true +} + +// CutSuffix returns collection without the provided ending suffix and reports +// whether it found the suffix. If collection doesn't end with suffix, CutSuffix returns collection, false. +// If suffix is empty, CutSuffix returns collection, true. +// Will iterate through the entire sequence and allocate a slice large enough to hold all elements. +// Long input sequences can cause excessive memory usage. +func CutSuffix[T comparable, I ~func(func(T) bool)](collection I, separator []T) (before I, found bool) { + slice := slices.Collect(iter.Seq[T](collection)) + result, ok := lo.CutSuffix(slice, separator) + return I(slices.Values(result)), ok +} + +// Trim removes all the leading and trailing cutset from the collection. +// Will allocate a map large enough to hold all distinct cutset elements. +func Trim[T comparable, I ~func(func(T) bool)](collection I, cutset ...T) I { + predicate := lo.Partial(lo.HasKey, lo.Keyify(cutset)) + return DropWhile(DropLastWhile(collection, predicate), predicate) +} + +// TrimFirst removes all the leading cutset from the collection. +// Will allocate a map large enough to hold all distinct cutset elements. +func TrimFirst[T comparable, I ~func(func(T) bool)](collection I, cutset ...T) I { + return DropWhile(collection, lo.Partial(lo.HasKey, lo.Keyify(cutset))) +} + +// TrimPrefix removes all the leading prefix from the collection. +func TrimPrefix[T comparable, I ~func(func(T) bool)](collection I, prefix []T) I { + n := len(prefix) + if n == 0 { + return collection + } + + return func(yield func(T) bool) { + var i int + for item := range collection { + if i < 0 { + if !yield(item) { + return + } + continue + } + + if item == prefix[i] { + i = (i + 1) % n + continue + } + + for j := 0; j < i; j++ { + if !yield(prefix[j]) { + return + } + } + if !yield(item) { + return + } + i = -1 + } + for j := 0; j < i; j++ { + if !yield(prefix[j]) { + return + } + } + } +} + +// TrimLast removes all the trailing cutset from the collection. +// Will allocate a map large enough to hold all distinct cutset elements. +func TrimLast[T comparable, I ~func(func(T) bool)](collection I, cutset ...T) I { + return DropLastWhile(collection, lo.Partial(lo.HasKey, lo.Keyify(cutset))) +} + +// TrimSuffix removes all the trailing suffix from the collection. +func TrimSuffix[T comparable, I ~func(func(T) bool)](collection I, suffix []T) I { + n := len(suffix) + if n == 0 { + return collection + } + + return func(yield func(T) bool) { + var i int + for item := range collection { + if item == suffix[i%n] { + i++ + } else { + for j := 0; j < i; j++ { + if !yield(suffix[j%n]) { + return + } + } + i = 0 + if item == suffix[i] { + i++ + } else if !yield(item) { + return + } + } + } + if i%n != 0 { + for j := 0; j < i; j++ { + if !yield(suffix[j%n]) { + return + } + } + } + } +} diff --git a/it/seq_benchmark_test.go b/it/seq_benchmark_test.go new file mode 100644 index 0000000..10c1887 --- /dev/null +++ b/it/seq_benchmark_test.go @@ -0,0 +1,160 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "iter" + "math/rand/v2" + "strconv" + "testing" +) + +var lengths = []int{10, 100, 1000} + +func BenchmarkChunk(b *testing.B) { + for _, n := range lengths { + strs := genStrings(n) + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Chunk(strs, 5) + } + }) + } + + for _, n := range lengths { + ints := genInts(n) + b.Run(fmt.Sprintf("ints%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Chunk(ints, 5) + } + }) + } +} + +func genStrings(n int) iter.Seq[string] { + return func(yield func(string) bool) { + for range n { + if !yield(strconv.Itoa(rand.IntN(100_000))) { + break + } + } + } +} + +func genInts(n int) iter.Seq[int] { + return func(yield func(int) bool) { + for range n { + if !yield(rand.IntN(100_000)) { + break + } + } + } +} + +func BenchmarkFlatten(b *testing.B) { + for _, n := range lengths { + ints := make([]iter.Seq[int], 0, n) + for i := 0; i < n; i++ { + ints = append(ints, genInts(n)) + } + b.Run(fmt.Sprintf("ints_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Flatten(ints) + } + }) + } + + for _, n := range lengths { + strs := make([]iter.Seq[string], 0, n) + for i := 0; i < n; i++ { + strs = append(strs, genStrings(n)) + } + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Flatten(strs) + } + }) + } +} + +func BenchmarkDrop(b *testing.B) { + for _, n := range lengths { + strs := genStrings(n) + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Drop(strs, n/4) + } + }) + } + + for _, n := range lengths { + ints := genInts(n) + b.Run(fmt.Sprintf("ints%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Drop(ints, n/4) + } + }) + } +} + +func BenchmarkDropWhile(b *testing.B) { + for _, n := range lengths { + strs := genStrings(n) + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = DropWhile(strs, func(v string) bool { return len(v) < 4 }) + } + }) + } + + for _, n := range lengths { + ints := genInts(n) + b.Run(fmt.Sprintf("ints%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = DropWhile(ints, func(v int) bool { return i < 10_000 }) + } + }) + } +} + +func BenchmarkDropByIndex(b *testing.B) { + for _, n := range lengths { + strs := genStrings(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 := genInts(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 { + strs := genStrings(n) + b.Run(fmt.Sprintf("strings_%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Replace(strs, "321321", "123123", 10) + } + }) + } + + for _, n := range lengths { + ints := genInts(n) + b.Run(fmt.Sprintf("ints%d", n), func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = Replace(ints, 321321, 123123, 10) + } + }) + } +} diff --git a/it/seq_example_test.go b/it/seq_example_test.go new file mode 100644 index 0000000..10a0b51 --- /dev/null +++ b/it/seq_example_test.go @@ -0,0 +1,692 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "iter" + "math" + "slices" + "strconv" +) + +func ExampleLength() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := Length(list) + + fmt.Printf("%v", result) + // Output: 4 +} + +func ExampleDrain() { + list := slices.Values([]int64{1, 2, 3, 4}) + + Drain(list) +} + +func ExampleFilter() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := Filter(list, func(nbr int64) bool { + return nbr%2 == 0 + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 4] +} + +func ExampleMap() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := Map(list, func(nbr int64) string { + return strconv.FormatInt(nbr*2, 10) + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 4 6 8] +} + +func ExampleUniqMap() { + type User struct { + Name string + Age int + } + users := slices.Values([]User{{Name: "Alex", Age: 10}, {Name: "Alex", Age: 12}, {Name: "Bob", Age: 11}, {Name: "Alice", Age: 20}}) + + result := UniqMap(users, func(u User) string { + return u.Name + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [Alex Bob Alice] +} + +func ExampleFilterMap() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := FilterMap(list, func(nbr int64) (string, bool) { + return strconv.FormatInt(nbr*2, 10), nbr%2 == 0 + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [4 8] +} + +func ExampleFlatMap() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := FlatMap(list, func(nbr int64) iter.Seq[string] { + return slices.Values([]string{ + strconv.FormatInt(nbr, 10), // base 10 + strconv.FormatInt(nbr, 2), // base 2 + }) + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [1 1 2 10 3 11 4 100] +} + +func ExampleReduce() { + list := slices.Values([]int64{1, 2, 3, 4}) + + result := Reduce(list, func(agg, item int64) int64 { + return agg + item + }, 0) + + fmt.Printf("%v", result) + // Output: 10 +} + +func ExampleReduceLast() { + list := slices.Values([][]int{{0, 1}, {2, 3}, {4, 5}}) + + result := ReduceLast(list, func(agg, item []int) []int { + return append(agg, item...) + }, []int{}) + + fmt.Printf("%v", result) + // Output: [4 5 2 3 0 1] +} + +func ExampleForEach() { + list := slices.Values([]int64{1, 2, 3, 4}) + + ForEach(list, func(x int64) { + fmt.Println(x) + }) + + // Output: + // 1 + // 2 + // 3 + // 4 +} + +func ExampleForEachWhile() { + list := slices.Values([]int64{1, 2, -math.MaxInt, 4}) + + ForEachWhile(list, func(x int64) 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) + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [0 1 2] +} + +func ExampleUniq() { + list := slices.Values([]int{1, 2, 2, 1}) + + result := Uniq(list) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [1 2] +} + +func ExampleUniqBy() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := UniqBy(list, func(i int) int { + return i % 3 + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [0 1 2] +} + +func ExampleGroupBy() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := GroupBy(list, func(i int) int { + return i % 3 + }) + + fmt.Printf("%v\n", result[0]) + fmt.Printf("%v\n", result[1]) + fmt.Printf("%v\n", result[2]) + // Output: + // [0 3] + // [1 4] + // [2 5] +} + +func ExampleGroupByMap() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := GroupByMap(list, func(i int) (int, int) { + return i % 3, i * 2 + }) + + fmt.Printf("%v\n", result[0]) + fmt.Printf("%v\n", result[1]) + fmt.Printf("%v\n", result[2]) + // Output: + // [0 6] + // [2 8] + // [4 10] +} + +func ExampleChunk() { + list := slices.Values([]int{0, 1, 2, 3, 4}) + + result := Chunk(list, 2) + + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // [0 1] + // [2 3] + // [4] +} + +func ExamplePartitionBy() { + list := slices.Values([]int{-2, -1, 0, 1, 2, 3, 4}) + + result := PartitionBy(list, func(x int) string { + if x < 0 { + return "negative" + } else if x%2 == 0 { + return "even" + } + return "odd" + }) + + for _, v := range result { + fmt.Printf("%v\n", v) + } + // Output: + // [-2 -1] + // [0 2 4] + // [1 3] +} + +func ExampleFlatten() { + list := []iter.Seq[int]{slices.Values([]int{0, 1, 2}), slices.Values([]int{3, 4, 5})} + + result := Flatten(list) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [0 1 2 3 4 5] +} + +func ExampleInterleave() { + list1 := []iter.Seq[int]{slices.Values([]int{1, 4, 7}), slices.Values([]int{2, 5, 8}), slices.Values([]int{3, 6, 9})} + list2 := []iter.Seq[int]{slices.Values([]int{1}), slices.Values([]int{2, 5, 8}), slices.Values([]int{3, 6}), slices.Values([]int{4, 7, 9, 10})} + + result1 := slices.Collect(Interleave(list1...)) + result2 := slices.Collect(Interleave(list2...)) + + fmt.Printf("%v\n", result1) + fmt.Printf("%v\n", result2) + // Output: + // [1 2 3 4 5 6 7 8 9] + // [1 2 3 4 5 6 7 8 9 10] +} + +func ExampleShuffle() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := slices.Collect(Shuffle(list)) + + fmt.Printf("%v", result) +} + +func ExampleReverse() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := slices.Collect(Reverse(list)) + + fmt.Printf("%v", result) + // Output: [5 4 3 2 1 0] +} + +func ExampleFill() { + list := slices.Values([]foo{{"a"}, {"a"}}) + + result := Fill(list, foo{"b"}) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [{b} {b}] +} + +func ExampleRepeat() { + result := Repeat(2, foo{"a"}) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [{a} {a}] +} + +func ExampleRepeatBy() { + result := RepeatBy(5, func(i int) string { + return strconv.FormatInt(int64(math.Pow(float64(i), 2)), 10) + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [0 1 4 9 16] +} + +func ExampleKeyBy() { + list := slices.Values([]string{"a", "aa", "aaa"}) + + result := KeyBy(list, func(str string) int { + return len(str) + }) + + fmt.Printf("%v", result) + // Output: map[1:a 2:aa 3:aaa] +} + +func ExampleSeqToMap() { + list := slices.Values([]string{"a", "aa", "aaa"}) + + result := SeqToMap(list, func(str string) (string, int) { + return str, len(str) + }) + + fmt.Printf("%v", result) + // Output: map[a:1 aa:2 aaa:3] +} + +func ExampleFilterSeqToMap() { + list := slices.Values([]string{"a", "aa", "aaa"}) + + result := FilterSeqToMap(list, func(str string) (string, int, bool) { + return str, len(str), len(str) > 1 + }) + + fmt.Printf("%v", result) + // Output: map[aa:2 aaa:3] +} + +func ExampleKeyify() { + list := slices.Values([]string{"a", "a", "b", "b", "d"}) + + set := Keyify(list) + _, ok1 := set["a"] + _, ok2 := set["c"] + fmt.Printf("%v\n", ok1) + fmt.Printf("%v\n", ok2) + fmt.Printf("%v\n", set) + + // Output: + // true + // false + // map[a:{} b:{} d:{}] +} + +func ExampleDrop() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := Drop(list, 2) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 3 4 5] +} + +func ExampleDropWhile() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := DropWhile(list, func(val int) bool { + return val < 2 + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 3 4 5] +} + +func ExampleDropByIndex() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := DropByIndex(list, 2) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [0 1 3 4 5] +} + +func ExampleReject() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := Reject(list, func(x int) bool { + return x%2 == 0 + }) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [1 3 5] +} + +func ExampleCount() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5, 0, 1, 2, 3}) + + result := Count(list, 2) + + fmt.Printf("%v", result) + // Output: 2 +} + +func ExampleCountBy() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5, 0, 1, 2, 3}) + + result := CountBy(list, func(i int) bool { + return i < 4 + }) + + fmt.Printf("%v", result) + // Output: 8 +} + +func ExampleCountValues() { + result1 := CountValues(slices.Values([]int{})) + result2 := CountValues(slices.Values([]int{1, 2})) + result3 := CountValues(slices.Values([]int{1, 2, 2})) + result4 := CountValues(slices.Values([]string{"foo", "bar", ""})) + result5 := CountValues(slices.Values([]string{"foo", "bar", "bar"})) + + fmt.Printf("%v\n", result1) + fmt.Printf("%v\n", result2) + fmt.Printf("%v\n", result3) + fmt.Printf("%v\n", result4) + fmt.Printf("%v\n", result5) + // Output: + // map[] + // map[1:1 2:1] + // map[1:1 2:2] + // map[:1 bar:1 foo:1] + // map[bar:2 foo:1] +} + +func ExampleCountValuesBy() { + isEven := func(v int) bool { + return v%2 == 0 + } + + result1 := CountValuesBy(slices.Values([]int{}), isEven) + result2 := CountValuesBy(slices.Values([]int{1, 2}), isEven) + result3 := CountValuesBy(slices.Values([]int{1, 2, 2}), isEven) + + length := func(v string) int { + return len(v) + } + + result4 := CountValuesBy(slices.Values([]string{"foo", "bar", ""}), length) + result5 := CountValuesBy(slices.Values([]string{"foo", "bar", "bar"}), length) + + fmt.Printf("%v\n", result1) + fmt.Printf("%v\n", result2) + fmt.Printf("%v\n", result3) + fmt.Printf("%v\n", result4) + fmt.Printf("%v\n", result5) + // Output: + // map[] + // map[false:1 true:1] + // map[false:1 true:2] + // map[0:1 3:2] + // map[3:3] +} + +func ExampleSubset() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5}) + + result := Subset(list, 2, 3) + + fmt.Printf("%v", slices.Collect(result)) + // Output: [2 3 4] +} + +func ExampleSlice() { + list := values(0, 1, 2, 3, 4, 5) + + result := Slice(list, 1, 4) + fmt.Printf("%v\n", slices.Collect(result)) + + result = Slice(list, 4, 1) + fmt.Printf("%v\n", slices.Collect(result)) + + result = Slice(list, 4, 5) + fmt.Printf("%v\n", slices.Collect(result)) + + // Output: + // [1 2 3] + // [] + // [4] +} + +func ExampleReplace() { + list := slices.Values([]int{0, 1, 0, 1, 2, 3, 0}) + + result := Replace(list, 0, 42, 1) + fmt.Printf("%v\n", slices.Collect(result)) + + result = Replace(list, -1, 42, 1) + fmt.Printf("%v\n", slices.Collect(result)) + + result = Replace(list, 0, 42, 2) + fmt.Printf("%v\n", slices.Collect(result)) + + result = Replace(list, 0, 42, -1) + fmt.Printf("%v\n", slices.Collect(result)) + + // Output: + // [42 1 0 1 2 3 0] + // [0 1 0 1 2 3 0] + // [42 1 42 1 2 3 0] + // [42 1 42 1 2 3 42] +} + +func ExampleCompact() { + list := slices.Values([]string{"", "foo", "", "bar", ""}) + + result := Compact(list) + + fmt.Printf("%v", slices.Collect(result)) + + // Output: [foo bar] +} + +func ExampleIsSorted() { + list := slices.Values([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}) + + result := IsSorted(list) + + fmt.Printf("%v", result) + + // Output: true +} + +func ExampleIsSortedBy() { + list := slices.Values([]string{"a", "bb", "ccc"}) + + result := IsSortedBy(list, func(s string) int { + return len(s) + }) + + fmt.Printf("%v", result) + + // Output: true +} + +func ExampleCutPrefix() { + collection := slices.Values([]string{"a", "b", "c", "d", "e", "f", "g"}) + + // Test with valid prefix + after, found := CutPrefix(collection, []string{"a", "b", "c"}) + fmt.Printf("After: %v, Found: %t\n", slices.Collect(after), found) + + // Test with prefix not found + after2, found2 := CutPrefix(collection, []string{"b"}) + fmt.Printf("After: %v, Found: %t\n", slices.Collect(after2), found2) + + // Test with empty prefix + after3, found3 := CutPrefix(collection, []string{}) + fmt.Printf("After: %v, Found: %t\n", slices.Collect(after3), found3) + + // Output: + // After: [d e f g], Found: true + // After: [a b c d e f g], Found: false + // After: [a b c d e f g], Found: true +} + +func ExampleCutSuffix() { + collection := slices.Values([]string{"a", "b", "c", "d", "e", "f", "g"}) + + // Test with valid suffix + before, found := CutSuffix(collection, []string{"f", "g"}) + fmt.Printf("Before: %v, Found: %t\n", slices.Collect(before), found) + + // Test with suffix not found + before2, found2 := CutSuffix(collection, []string{"b"}) + fmt.Printf("Before: %v, Found: %t\n", slices.Collect(before2), found2) + + // Test with empty suffix + before3, found3 := CutSuffix(collection, []string{}) + fmt.Printf("Before: %v, Found: %t\n", slices.Collect(before3), found3) + + // Output: + // Before: [a b c d e], Found: true + // Before: [a b c d e f g], Found: false + // Before: [a b c d e f g], Found: true +} + +func ExampleTrim() { + collection := slices.Values([]int{0, 1, 2, 0, 3, 0}) + + // Test with valid cutset + result := Trim(collection, 0) + fmt.Printf("Trim with cutset {0}: %v\n", slices.Collect(result)) + + // Test with string collection + words := slices.Values([]string{" hello ", "world", " "}) + result2 := Trim(words, " ") + fmt.Printf("Trim with string cutset: %v\n", slices.Collect(result2)) + + // Test with no cutset elements + result3 := Trim(collection, 5) + fmt.Printf("Trim with cutset {5} (not present): %v\n", slices.Collect(result3)) + + // Output: + // Trim with cutset {0}: [1 2 0 3] + // Trim with string cutset: [ hello world ] + // Trim with cutset {5} (not present): [0 1 2 0 3 0] +} + +func ExampleTrimFirst() { + collection := slices.Values([]int{0, 1, 2, 0, 3, 0}) + + // Test with valid cutset + result := TrimFirst(collection, 0) + fmt.Printf("TrimFirst with cutset {0}: %v\n", slices.Collect(result)) + + // Test with string collection + words := slices.Values([]string{" hello ", "world", " "}) + result2 := TrimFirst(words, " ") + fmt.Printf("TrimFirst with string cutset: %v\n", slices.Collect(result2)) + + // Test with no cutset elements + result3 := TrimFirst(collection, 5) + fmt.Printf("TrimFirst with cutset {5} (not present): %v\n", slices.Collect(result3)) + + // Output: + // TrimFirst with cutset {0}: [1 2 0 3 0] + // TrimFirst with string cutset: [ hello world ] + // TrimFirst with cutset {5} (not present): [0 1 2 0 3 0] +} + +func ExampleTrimPrefix() { + collection := slices.Values([]int{1, 2, 1, 2, 3}) + + // Test with valid prefix + result := TrimPrefix(collection, []int{1, 2}) + fmt.Printf("TrimPrefix with prefix {1,2}: %v\n", slices.Collect(result)) + + // Test with string collection + words := slices.Values([]string{"hello", "hello", "world"}) + result2 := TrimPrefix(words, []string{"hello"}) + fmt.Printf("TrimPrefix with string prefix: %v\n", slices.Collect(result2)) + + // Test with prefix not present + result3 := TrimPrefix(collection, []int{5, 6}) + fmt.Printf("TrimPrefix with prefix {5,6} (not present): %v\n", slices.Collect(result3)) + + // Output: + // TrimPrefix with prefix {1,2}: [3] + // TrimPrefix with string prefix: [world] + // TrimPrefix with prefix {5,6} (not present): [1 2 1 2 3] +} + +func ExampleTrimLast() { + collection := slices.Values([]int{0, 1, 2, 0, 3, 0}) + + // Test with valid cutset + result := TrimLast(collection, 0) + fmt.Printf("TrimLast with cutset {0}: %v\n", slices.Collect(result)) + + // Test with string collection + words := slices.Values([]string{" hello ", "world", " "}) + result2 := TrimLast(words, " ") + fmt.Printf("TrimLast with string cutset: %v\n", slices.Collect(result2)) + + // Test with no cutset elements + result3 := TrimLast(collection, 5) + fmt.Printf("TrimLast with cutset {5} (not present): %v\n", slices.Collect(result3)) + + // Output: + // TrimLast with cutset {0}: [0 1 2 0 3] + // TrimLast with string cutset: [ hello world ] + // TrimLast with cutset {5} (not present): [0 1 2 0 3 0] +} + +func ExampleTrimSuffix() { + collection := slices.Values([]int{1, 2, 1, 2, 3}) + + // Test with valid suffix + result := TrimSuffix(collection, []int{1, 2}) + fmt.Printf("TrimSuffix with suffix {1,2}: %v\n", slices.Collect(result)) + + // Test with string collection + words := slices.Values([]string{"hello", "world", "test"}) + result2 := TrimSuffix(words, []string{"test"}) + fmt.Printf("TrimSuffix with string suffix: %v\n", slices.Collect(result2)) + + // Test with suffix not present + result3 := TrimSuffix(collection, []int{5, 6}) + fmt.Printf("TrimSuffix with suffix {5,6} (not present): %v\n", slices.Collect(result3)) + + // Output: + // TrimSuffix with suffix {1,2}: [1 2 1 2 3] + // TrimSuffix with string suffix: [hello world] + // TrimSuffix with suffix {5,6} (not present): [1 2 1 2 3] +} diff --git a/it/seq_test.go b/it/seq_test.go new file mode 100644 index 0000000..11ac073 --- /dev/null +++ b/it/seq_test.go @@ -0,0 +1,1389 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "iter" + "math" + "slices" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLength(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Length(values[int]()) + r2 := Length(values(1, 2, 3, 4)) + + is.Zero(r1) + is.Equal(4, r2) +} + +func TestDrain(t *testing.T) { + t.Parallel() + is := assert.New(t) + + var done bool + list := iter.Seq[int](func(yield func(int) bool) { + yield(1) + yield(2) + yield(3) + done = true + }) + + Drain(list) + + is.True(done) +} + +func TestFilter(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Filter(values(1, 2, 3, 4), func(x int) bool { + return x%2 == 0 + }) + is.Equal([]int{2, 4}, slices.Collect(r1)) + + r2 := Filter(values("", "foo", "", "bar", ""), func(x string) bool { + return len(x) > 0 + }) + is.Equal([]string{"foo", "bar"}, slices.Collect(r2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Filter(allStrings, func(x string) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestFilterI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := FilterI(values(1, 2, 3, 4), func(x, _ int) bool { + return x%2 == 0 + }) + is.Equal([]int{2, 4}, slices.Collect(r1)) + + r2 := FilterI(values("", "foo", "", "bar", ""), func(x string, _ int) bool { + return len(x) > 0 + }) + is.Equal([]string{"foo", "bar"}, slices.Collect(r2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := FilterI(allStrings, func(x string, _ int) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Map(values(1, 2, 3, 4), func(x int) string { + return "Hello" + }) + result2 := Map(values[int64](1, 2, 3, 4), func(x int64) string { + return strconv.FormatInt(x, 10) + }) + + is.Equal([]string{"Hello", "Hello", "Hello", "Hello"}, slices.Collect(result1)) + is.Equal([]string{"1", "2", "3", "4"}, slices.Collect(result2)) +} + +func TestMapI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := MapI(values(1, 2, 3, 4), func(x, _ int) string { + return "Hello" + }) + result2 := MapI(values[int64](1, 2, 3, 4), func(x int64, _ int) string { + return strconv.FormatInt(x, 10) + }) + + is.Equal([]string{"Hello", "Hello", "Hello", "Hello"}, slices.Collect(result1)) + is.Equal([]string{"1", "2", "3", "4"}, slices.Collect(result2)) +} + +func TestUniqMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + Name string + age int + } + + users := values(User{Name: "Alice", age: 20}, User{Name: "Alex", age: 21}, User{Name: "Alex", age: 22}) + result := UniqMap(users, func(item User) string { + return item.Name + }) + + is.Equal([]string{"Alice", "Alex"}, slices.Collect(result)) +} + +func TestUniqMapI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + type User struct { + Name string + age int + } + + users := values(User{Name: "Alice", age: 20}, User{Name: "Alex", age: 21}, User{Name: "Alex", age: 22}) + result := UniqMapI(users, func(item User, _ int) string { + return item.Name + }) + + is.Equal([]string{"Alice", "Alex"}, slices.Collect(result)) +} + +func TestFilterMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := FilterMap(values[int64](1, 2, 3, 4), func(x int64) (string, bool) { + if x%2 == 0 { + return strconv.FormatInt(x, 10), true + } + return "", false + }) + r2 := FilterMap(values("cpu", "gpu", "mouse", "keyboard"), func(x string) (string, bool) { + if strings.HasSuffix(x, "pu") { + return "xpu", true + } + return "", false + }) + + is.Equal([]string{"2", "4"}, slices.Collect(r1)) + is.Equal([]string{"xpu", "xpu"}, slices.Collect(r2)) +} + +func TestFilterMapI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := FilterMapI(values[int64](1, 2, 3, 4), func(x int64, _ int) (string, bool) { + if x%2 == 0 { + return strconv.FormatInt(x, 10), true + } + return "", false + }) + r2 := FilterMapI(values("cpu", "gpu", "mouse", "keyboard"), func(x string, _ int) (string, bool) { + if strings.HasSuffix(x, "pu") { + return "xpu", true + } + return "", false + }) + + is.Equal([]string{"2", "4"}, slices.Collect(r1)) + is.Equal([]string{"xpu", "xpu"}, slices.Collect(r2)) +} + +func TestFlatMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FlatMap(values(0, 1, 2, 3, 4), func(x int) iter.Seq[string] { + return values("Hello") + }) + result2 := FlatMap(values[int64](0, 1, 2, 3, 4), func(x int64) iter.Seq[string] { + return func(yield func(string) bool) { + for range x { + if !yield(strconv.FormatInt(x, 10)) { + return + } + } + } + }) + + is.Equal([]string{"Hello", "Hello", "Hello", "Hello", "Hello"}, slices.Collect(result1)) + is.Equal([]string{"1", "2", "2", "3", "3", "3", "4", "4", "4", "4"}, slices.Collect(result2)) +} + +func TestFlatMapI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := FlatMapI(values(0, 1, 2, 3, 4), func(x, _ int) iter.Seq[string] { + return values("Hello") + }) + result2 := FlatMapI(values[int64](0, 1, 2, 3, 4), func(x int64, _ int) iter.Seq[string] { + return func(yield func(string) bool) { + for range x { + if !yield(strconv.FormatInt(x, 10)) { + return + } + } + } + }) + + is.Equal([]string{"Hello", "Hello", "Hello", "Hello", "Hello"}, slices.Collect(result1)) + is.Equal([]string{"1", "2", "2", "3", "3", "3", "4", "4", "4", "4"}, slices.Collect(result2)) +} + +func TestTimes(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Times(3, func(i int) string { + return strconv.FormatInt(int64(i), 10) + }) + is.Equal([]string{"0", "1", "2"}, slices.Collect(result1)) +} + +func TestReduce(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Reduce(values(1, 2, 3, 4), func(agg, item int) int { + return agg + item + }, 0) + result2 := Reduce(values(1, 2, 3, 4), func(agg, item int) int { + return agg + item + }, 10) + + is.Equal(10, result1) + is.Equal(20, result2) +} + +func TestReduceI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ReduceI(values(1, 2, 3, 4), func(agg, item, _ int) int { + return agg + item + }, 0) + result2 := ReduceI(values(1, 2, 3, 4), func(agg, item, _ int) int { + return agg + item + }, 10) + + is.Equal(10, result1) + is.Equal(20, result2) +} + +func TestReduceLast(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ReduceLast(values([]int{0, 1}, []int{2, 3}, []int{4, 5}), func(agg, item []int) []int { + return append(agg, item...) + }, []int{}) + is.Equal([]int{4, 5, 2, 3, 0, 1}, result1) + + result2 := ReduceLast(values(1, 2, 3, 4), func(agg, item int) int { + return agg + item + }, 10) + is.Equal(20, result2) +} + +func TestReduceLastI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ReduceLastI(values([]int{0, 1}, []int{2, 3}, []int{4, 5}), func(agg, item []int, _ int) []int { + return append(agg, item...) + }, []int{}) + is.Equal([]int{4, 5, 2, 3, 0, 1}, result1) + + result2 := ReduceLastI(values(1, 2, 3, 4), func(agg, item, _ int) int { + return agg + item + }, 10) + is.Equal(20, result2) +} + +func TestForEachI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + // check of callback is called for every element and in proper order + + callParams1 := []string{} + callParams2 := []int{} + + ForEachI(values("a", "b", "c"), func(item string, i int) { + callParams1 = append(callParams1, item) + callParams2 = append(callParams2, i) + }) + + is.Equal([]string{"a", "b", "c"}, callParams1) + is.Equal([]int{0, 1, 2}, callParams2) + is.IsIncreasing(callParams2) +} + +func TestForEachWhileI(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 + + ForEachWhileI(values("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.Equal([]string{"a", "b"}, callParams1) + is.Equal([]int{0, 1}, callParams2) + is.IsIncreasing(callParams2) +} + +func TestUniq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Uniq(values(1, 2, 2, 1)) + is.Equal([]int{1, 2}, slices.Collect(result1)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Uniq(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestUniqBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := UniqBy(values(0, 1, 2, 3, 4, 5), func(i int) int { + return i % 3 + }) + is.Equal([]int{0, 1, 2}, slices.Collect(result1)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := UniqBy(allStrings, func(i string) string { + return i + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestGroupBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := GroupBy(values(0, 1, 2, 3, 4, 5), func(i int) int { + return i % 3 + }) + is.Equal(map[int][]int{ + 0: {0, 3}, + 1: {1, 4}, + 2: {2, 5}, + }, result1) +} + +func TestGroupByMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := GroupByMap(values(0, 1, 2, 3, 4, 5), func(i int) (int, string) { + return i % 3, strconv.Itoa(i) + }) + is.Equal(map[int][]string{ + 0: {"0", "3"}, + 1: {"1", "4"}, + 2: {"2", "5"}, + }, result1) + + type myInt int + result2 := GroupByMap(values[myInt](1, 0, 2, 3, 4, 5), func(i myInt) (int, string) { + return int(i % 3), strconv.Itoa(int(i)) + }) + is.Equal(map[int][]string{ + 0: {"0", "3"}, + 1: {"1", "4"}, + 2: {"2", "5"}, + }, result2) + + type product struct { + ID int64 + CategoryID int64 + } + products := values( + product{ID: 1, CategoryID: 1}, + product{ID: 2, CategoryID: 1}, + product{ID: 3, CategoryID: 2}, + product{ID: 4, CategoryID: 3}, + product{ID: 5, CategoryID: 3}, + ) + result3 := GroupByMap(products, func(item product) (int64, string) { + return item.CategoryID, "Product " + strconv.FormatInt(item.ID, 10) + }) + is.Equal(map[int64][]string{ + 1: {"Product 1", "Product 2"}, + 2: {"Product 3"}, + 3: {"Product 4", "Product 5"}, + }, result3) +} + +func TestChunk(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Chunk(values(0, 1, 2, 3, 4, 5), 2) + result2 := Chunk(values(0, 1, 2, 3, 4, 5, 6), 2) + result3 := Chunk(values[int](), 2) + result4 := Chunk(values(0), 2) + + is.Equal([][]int{{0, 1}, {2, 3}, {4, 5}}, slices.Collect(result1)) + is.Equal([][]int{{0, 1}, {2, 3}, {4, 5}, {6}}, slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + is.Equal([][]int{{0}}, slices.Collect(result4)) + is.PanicsWithValue("it.Chunk: size must be greater than 0", func() { + Chunk(values(0), 0) + }) +} + +func TestPartitionBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + oddEven := func(x int) string { + if x < 0 { + return "negative" + } else if x%2 == 0 { + return "even" + } + return "odd" + } + + result1 := PartitionBy(values(-2, -1, 0, 1, 2, 3, 4, 5), oddEven) + result2 := PartitionBy(values[int](), oddEven) + + is.Equal([][]int{{-2, -1}, {0, 2, 4}, {1, 3, 5}}, result1) + is.Empty(result2) +} + +func TestFlatten(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Flatten([]iter.Seq[int]{values(0, 1), values(2, 3, 4, 5)}) + + is.Equal([]int{0, 1, 2, 3, 4, 5}, slices.Collect(result1)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Flatten([]myStrings{allStrings}) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestInterleave(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in []iter.Seq[int] + want []int + }{ + { + "empty", + []iter.Seq[int]{}, + nil, + }, + { + "empties", + []iter.Seq[int]{values[int](), values[int]()}, + nil, + }, + { + "same length", + []iter.Seq[int]{values(1, 3, 5), values(2, 4, 6)}, + []int{1, 2, 3, 4, 5, 6}, + }, + { + "different length", + []iter.Seq[int]{values(1, 3, 5, 6), values(2, 4)}, + []int{1, 2, 3, 4, 5, 6}, + }, + { + "many sequences", + []iter.Seq[int]{values(1), values(2, 5, 8), values(3, 6), values(4, 7, 9, 10)}, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, slices.Collect(Interleave(tc.in...))) + }) + } +} + +func TestShuffle(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Shuffle(values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) + result2 := Shuffle(values[int]()) + + slice1 := slices.Collect(result1) + is.NotEqual([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, slice1) + is.ElementsMatch([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, slice1) + is.Empty(slices.Collect(result2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Shuffle(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestReverse(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Reverse(values(0, 1, 2, 3, 4, 5)) + result2 := Reverse(values(0, 1, 2, 3, 4, 5, 6)) + result3 := Reverse(values[int]()) + + is.Equal([]int{5, 4, 3, 2, 1, 0}, slices.Collect(result1)) + is.Equal([]int{6, 5, 4, 3, 2, 1, 0}, slices.Collect(result2)) + is.Empty(slices.Collect(result3)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Reverse(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestFill(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Fill(values(foo{"a"}, foo{"a"}), foo{"b"}) + result2 := Fill(values[foo](), foo{"a"}) + + is.Equal([]foo{{"b"}, {"b"}}, slices.Collect(result1)) + is.Empty(slices.Collect(result2)) +} + +func TestRepeat(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Repeat(2, foo{"a"}) + result2 := Repeat(0, foo{"a"}) + + is.Equal([]foo{{"a"}, {"a"}}, slices.Collect(result1)) + is.Empty(slices.Collect(result2)) +} + +func TestRepeatBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + cb := func(i int) int { + return int(math.Pow(float64(i), 2)) + } + + result1 := RepeatBy(0, cb) + result2 := RepeatBy(2, cb) + result3 := RepeatBy(5, cb) + + is.Empty(slices.Collect(result1)) + is.Equal([]int{0, 1}, slices.Collect(result2)) + is.Equal([]int{0, 1, 4, 9, 16}, slices.Collect(result3)) +} + +func TestKeyBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := KeyBy(values("a", "aa", "aaa"), func(str string) int { + return len(str) + }) + + is.Equal(map[int]string{1: "a", 2: "aa", 3: "aaa"}, result1) +} + +func TestAssociate(t *testing.T) { + t.Parallel() + + type foo struct { + baz string + bar int + } + transform := func(f *foo) (string, int) { + return f.baz, f.bar + } + testCases := []struct { + in []*foo + want map[string]int + }{ + { + in: []*foo{{baz: "apple", bar: 1}}, + want: map[string]int{"apple": 1}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}}, + want: map[string]int{"apple": 1, "banana": 2}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "apple", bar: 2}}, + want: map[string]int{"apple": 2}, + }, + } + for i, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, Associate(slices.Values(tc.in), transform)) + }) + } +} + +func TestSeqToMap(t *testing.T) { + t.Parallel() + + type foo struct { + baz string + bar int + } + transform := func(f *foo) (string, int) { + return f.baz, f.bar + } + testCases := []struct { + in []*foo + want map[string]int + }{ + { + in: []*foo{{baz: "apple", bar: 1}}, + want: map[string]int{"apple": 1}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}}, + want: map[string]int{"apple": 1, "banana": 2}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "apple", bar: 2}}, + want: map[string]int{"apple": 2}, + }, + } + for i, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, SeqToMap(slices.Values(tc.in), transform)) + }) + } +} + +func TestFilterSeqToMap(t *testing.T) { + t.Parallel() + + type foo struct { + baz string + bar int + } + transform := func(f *foo) (string, int, bool) { + return f.baz, f.bar, f.bar > 1 + } + testCases := []struct { + in []*foo + want map[string]int + }{ + { + in: []*foo{{baz: "apple", bar: 1}}, + want: map[string]int{}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "banana", bar: 2}}, + want: map[string]int{"banana": 2}, + }, + { + in: []*foo{{baz: "apple", bar: 1}, {baz: "apple", bar: 2}}, + want: map[string]int{"apple": 2}, + }, + } + for i, tc := range testCases { + tc := tc + t.Run(fmt.Sprintf("test_%d", i), func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, FilterSeqToMap(slices.Values(tc.in), transform)) + }) + } +} + +func TestKeyify(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := Keyify(values(1, 2, 3, 4)) + result2 := Keyify(values(1, 1, 1, 2)) + result3 := Keyify(values[int]()) + is.Equal(map[int]struct{}{1: {}, 2: {}, 3: {}, 4: {}}, result1) + is.Equal(map[int]struct{}{1: {}, 2: {}}, result2) + is.Empty(result3) +} + +func TestDrop(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(Drop(values(0, 1, 2, 3, 4), 0))) + is.Equal([]int{1, 2, 3, 4}, slices.Collect(Drop(values(0, 1, 2, 3, 4), 1))) + is.Equal([]int{2, 3, 4}, slices.Collect(Drop(values(0, 1, 2, 3, 4), 2))) + is.Equal([]int{3, 4}, slices.Collect(Drop(values(0, 1, 2, 3, 4), 3))) + is.Equal([]int{4}, slices.Collect(Drop(values(0, 1, 2, 3, 4), 4))) + is.Empty(slices.Collect(Drop(values(0, 1, 2, 3, 4), 5))) + is.Empty(slices.Collect(Drop(values(0, 1, 2, 3, 4), 6))) + + is.PanicsWithValue("it.Drop: n must not be negative", func() { + Drop(values(0, 1, 2, 3, 4), -1) + }) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Drop(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestDropLast(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(DropLast(values(0, 1, 2, 3, 4), 0))) + is.Equal([]int{0, 1, 2, 3}, slices.Collect(DropLast(values(0, 1, 2, 3, 4), 1))) + is.Equal([]int{0, 1, 2}, slices.Collect(DropLast(values(0, 1, 2, 3, 4), 2))) + is.Equal([]int{0, 1}, slices.Collect(DropLast(values(0, 1, 2, 3, 4), 3))) + is.Equal([]int{0}, slices.Collect(DropLast(values(0, 1, 2, 3, 4), 4))) + is.Empty(slices.Collect(DropLast(values(0, 1, 2, 3, 4), 5))) + is.Empty(slices.Collect(DropLast(values(0, 1, 2, 3, 4), 6))) + + is.PanicsWithValue("it.DropLast: n must not be negative", func() { + DropLast(values(0, 1, 2, 3, 4), -1) + }) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := DropLast(allStrings, 2) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestDropWhile(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Equal([]int{4, 5, 6}, slices.Collect(DropWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t != 4 + }))) + + is.Empty(slices.Collect(DropWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return true + }))) + + is.Equal([]int{0, 1, 2, 3, 4, 5, 6}, slices.Collect(DropWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t == 10 + }))) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := DropWhile(allStrings, func(t string) bool { + return t != "foo" + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestDropLastWhile(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Equal([]int{0, 1, 2, 3}, slices.Collect(DropLastWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t != 3 + }))) + + is.Equal([]int{0, 1}, slices.Collect(DropLastWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t != 1 + }))) + + is.Equal([]int{0, 1, 2, 3, 4, 5, 6}, slices.Collect(DropLastWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t == 10 + }))) + + is.Empty(slices.Collect(DropLastWhile(values(0, 1, 2, 3, 4, 5, 6), func(t int) bool { + return t != 10 + }))) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := DropLastWhile(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}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 0))) + is.Equal([]int{3, 4}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 0, 1, 2))) + is.Equal([]int{2, 4}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 3, 1, 0))) + is.Equal([]int{0, 1, 3, 4}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 2))) + is.Equal([]int{0, 1, 2, 3}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 4))) + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 5))) + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(DropByIndex(values(0, 1, 2, 3, 4), 100))) + is.Empty(slices.Collect(DropByIndex(values[int](), 0, 1))) + is.Empty(slices.Collect(DropByIndex(values(42), 0, 1))) + is.Empty(slices.Collect(DropByIndex(values(42), 1, 0))) + is.Empty(slices.Collect(DropByIndex(values[int](), 1))) + is.Empty(slices.Collect(DropByIndex(values(1), 0))) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := DropByIndex(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestReject(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Reject(values(1, 2, 3, 4), func(x int) bool { + return x%2 == 0 + }) + + is.Equal([]int{1, 3}, slices.Collect(r1)) + + r2 := Reject(values("Smith", "foo", "Domin", "bar", "Olivia"), func(x string) bool { + return len(x) > 3 + }) + + is.Equal([]string{"foo", "bar"}, slices.Collect(r2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Reject(allStrings, func(x string) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestRejectI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := RejectI(values(1, 2, 3, 4), func(x, _ int) bool { + return x%2 == 0 + }) + + is.Equal([]int{1, 3}, slices.Collect(r1)) + + r2 := RejectI(values("Smith", "foo", "Domin", "bar", "Olivia"), func(x string, _ int) bool { + return len(x) > 3 + }) + + is.Equal([]string{"foo", "bar"}, slices.Collect(r2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := RejectI(allStrings, func(x string, _ int) bool { + return len(x) > 0 + }) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestRejectMap(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := RejectMap(values[int64](1, 2, 3, 4), func(x int64) (string, bool) { + if x%2 == 0 { + return strconv.FormatInt(x, 10), false + } + return "", true + }) + r2 := RejectMap(values("cpu", "gpu", "mouse", "keyboard"), func(x string) (string, bool) { + if strings.HasSuffix(x, "pu") { + return "xpu", false + } + return "", true + }) + + is.Equal([]string{"2", "4"}, slices.Collect(r1)) + is.Equal([]string{"xpu", "xpu"}, slices.Collect(r2)) +} + +func TestRejectMapI(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := RejectMapI(values[int64](1, 2, 3, 4), func(x int64, _ int) (string, bool) { + if x%2 == 0 { + return strconv.FormatInt(x, 10), false + } + return "", true + }) + r2 := RejectMapI(values("cpu", "gpu", "mouse", "keyboard"), func(x string, _ int) (string, bool) { + if strings.HasSuffix(x, "pu") { + return "xpu", false + } + return "", true + }) + + is.Equal([]string{"2", "4"}, slices.Collect(r1)) + is.Equal([]string{"xpu", "xpu"}, slices.Collect(r2)) +} + +func TestCount(t *testing.T) { + t.Parallel() + is := assert.New(t) + + count1 := Count(values(1, 2, 1), 1) + count2 := Count(values(1, 2, 1), 3) + count3 := Count(values[int](), 1) + + is.Equal(2, count1) + is.Zero(count2) + is.Zero(count3) +} + +func TestCountBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + count1 := CountBy(values(1, 2, 1), func(i int) bool { + return i < 2 + }) + count2 := CountBy(values(1, 2, 1), func(i int) bool { + return i > 2 + }) + count3 := CountBy(values[int](), func(i int) bool { + return i <= 2 + }) + + is.Equal(2, count1) + is.Zero(count2) + is.Zero(count3) +} + +func TestCountValues(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Empty(CountValues(values[int]())) + is.Equal(map[int]int{1: 1, 2: 1}, CountValues(values(1, 2))) + is.Equal(map[int]int{1: 1, 2: 2}, CountValues(values(1, 2, 2))) + is.Equal(map[string]int{"": 1, "foo": 1, "bar": 1}, CountValues(values("foo", "bar", ""))) + is.Equal(map[string]int{"foo": 1, "bar": 2}, CountValues(values("foo", "bar", "bar"))) +} + +func TestCountValuesBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + oddEven := func(v int) bool { + return v%2 == 0 + } + length := func(v string) int { + return len(v) + } + + result1 := CountValuesBy(values[int](), oddEven) + result2 := CountValuesBy(values(1, 2), oddEven) + result3 := CountValuesBy(values(1, 2, 2), oddEven) + result4 := CountValuesBy(values("foo", "bar", ""), length) + result5 := CountValuesBy(values("foo", "bar", "bar"), length) + + is.Empty(result1) + is.Equal(map[bool]int{true: 1, false: 1}, result2) + is.Equal(map[bool]int{true: 2, false: 1}, result3) + is.Equal(map[int]int{0: 1, 3: 2}, result4) + is.Equal(map[int]int{3: 3}, result5) +} + +func TestSubset(t *testing.T) { + t.Parallel() + is := assert.New(t) + + in := values(0, 1, 2, 3, 4) + + out1 := Subset(in, 0, 0) + out2 := Subset(in, 10, 2) + out4 := Subset(in, 0, 10) + out5 := Subset(in, 0, 2) + out6 := Subset(in, 2, 2) + out7 := Subset(in, 2, 5) + out8 := Subset(in, 2, 3) + out9 := Subset(in, 2, 4) + + is.Empty(slices.Collect(out1)) + is.Empty(slices.Collect(out2)) + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(out4)) + is.Equal([]int{0, 1}, slices.Collect(out5)) + is.Equal([]int{2, 3}, slices.Collect(out6)) + is.Equal([]int{2, 3, 4}, slices.Collect(out7)) + is.Equal([]int{2, 3, 4}, slices.Collect(out8)) + is.Equal([]int{2, 3, 4}, slices.Collect(out9)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Subset(allStrings, 0, 2) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestSlice(t *testing.T) { + t.Parallel() + is := assert.New(t) + + in := values(0, 1, 2, 3, 4) + + out1 := Slice(in, 0, 0) + out2 := Slice(in, 0, 1) + out3 := Slice(in, 0, 5) + out4 := Slice(in, 0, 6) + out5 := Slice(in, 1, 1) + out6 := Slice(in, 1, 5) + out7 := Slice(in, 1, 6) + out8 := Slice(in, 4, 5) + out9 := Slice(in, 5, 5) + out10 := Slice(in, 6, 5) + out11 := Slice(in, 6, 6) + out12 := Slice(in, 1, 0) + out13 := Slice(in, 5, 0) + out14 := Slice(in, 6, 4) + out15 := Slice(in, 6, 7) + + is.Empty(slices.Collect(out1)) + is.Equal([]int{0}, slices.Collect(out2)) + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(out3)) + is.Equal([]int{0, 1, 2, 3, 4}, slices.Collect(out4)) + is.Empty(slices.Collect(out5)) + is.Equal([]int{1, 2, 3, 4}, slices.Collect(out6)) + is.Equal([]int{1, 2, 3, 4}, slices.Collect(out7)) + is.Equal([]int{4}, slices.Collect(out8)) + is.Empty(slices.Collect(out9)) + is.Empty(slices.Collect(out10)) + is.Empty(slices.Collect(out11)) + is.Empty(slices.Collect(out12)) + is.Empty(slices.Collect(out13)) + is.Empty(slices.Collect(out14)) + is.Empty(slices.Collect(out15)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Slice(allStrings, 0, 2) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestReplace(t *testing.T) { + t.Parallel() + is := assert.New(t) + + in := values(0, 1, 0, 1, 2, 3, 0) + + out1 := Replace(in, 0, 42, 2) + out2 := Replace(in, 0, 42, 1) + out3 := Replace(in, 0, 42, 0) + out4 := Replace(in, 0, 42, -1) + out5 := Replace(in, 0, 42, -1) + out6 := Replace(in, -1, 42, 2) + out7 := Replace(in, -1, 42, 1) + out8 := Replace(in, -1, 42, 0) + out9 := Replace(in, -1, 42, -1) + out10 := Replace(in, -1, 42, -1) + + is.Equal([]int{42, 1, 42, 1, 2, 3, 0}, slices.Collect(out1)) + is.Equal([]int{42, 1, 0, 1, 2, 3, 0}, slices.Collect(out2)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out3)) + is.Equal([]int{42, 1, 42, 1, 2, 3, 42}, slices.Collect(out4)) + is.Equal([]int{42, 1, 42, 1, 2, 3, 42}, slices.Collect(out5)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out6)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out7)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out8)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out9)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out10)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Replace(allStrings, "0", "2", 1) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestReplaceAll(t *testing.T) { + t.Parallel() + is := assert.New(t) + + in := values(0, 1, 0, 1, 2, 3, 0) + + out1 := ReplaceAll(in, 0, 42) + out2 := ReplaceAll(in, -1, 42) + + is.Equal([]int{42, 1, 42, 1, 2, 3, 42}, slices.Collect(out1)) + is.Equal([]int{0, 1, 0, 1, 2, 3, 0}, slices.Collect(out2)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := ReplaceAll(allStrings, "0", "2") + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestCompact(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Compact(values(2, 0, 4, 0)) + + is.Equal([]int{2, 4}, slices.Collect(r1)) + + r2 := Compact(values("", "foo", "", "bar", "")) + + is.Equal([]string{"foo", "bar"}, slices.Collect(r2)) + + r3 := Compact(values(true, false, true, false)) + + is.Equal([]bool{true, true}, slices.Collect(r3)) + + type foo struct { + bar int + baz string + } + + // sequence of structs + // If all fields of an element are zero values, Compact removes it. + + r4 := Compact(values( + foo{bar: 1, baz: "a"}, // all fields are non-zero values + foo{bar: 0, baz: ""}, // all fields are zero values + foo{bar: 2, baz: ""}, // bar is non-zero + )) + + is.Equal([]foo{{bar: 1, baz: "a"}, {bar: 2, baz: ""}}, slices.Collect(r4)) + + // sequence of pointers to structs + // If an element is nil, Compact removes it. + + e1, e2, e3 := foo{bar: 1, baz: "a"}, foo{bar: 0, baz: ""}, foo{bar: 2, baz: ""} + // NOTE: e2 is a zero value of foo, but its pointer &e2 is not a zero value of *foo. + r5 := Compact(values(&e1, &e2, nil, &e3)) + + is.Equal([]*foo{&e1, &e2, &e3}, slices.Collect(r5)) + + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Compact(allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestIsSorted(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.True(IsSorted(values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9))) + is.True(IsSorted(values("a", "b", "c", "d", "e", "f", "g", "h", "i", "j"))) + + is.False(IsSorted(values(0, 1, 4, 3, 2, 5, 6, 7, 8, 9, 10))) + is.False(IsSorted(values("a", "b", "d", "c", "e", "f", "g", "h", "i", "j"))) +} + +func TestIsSortedBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.True(IsSortedBy(values("a", "bb", "ccc"), func(s string) int { + return len(s) + })) + + is.False(IsSortedBy(values("aa", "b", "ccc"), func(s string) int { + return len(s) + })) + + is.True(IsSortedBy(values("1", "2", "3", "11"), func(s string) int { + ret, _ := strconv.Atoi(s) + return ret + })) +} + +func TestSplice(t *testing.T) { + t.Parallel() + is := assert.New(t) + + sample := values("a", "b", "c", "d", "e", "f", "g") + + // normal case + results := slices.Collect(Splice(sample, 1, "1", "2")) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(sample)) + is.Equal([]string{"a", "1", "2", "b", "c", "d", "e", "f", "g"}, results) + + // positive overflow + results = slices.Collect(Splice(sample, 42, "1", "2")) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(sample)) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g", "1", "2"}, results) + + // other + is.Equal([]string{"1", "2"}, slices.Collect(Splice(values[string](), 0, "1", "2"))) + is.Equal([]string{"1", "2"}, slices.Collect(Splice(values[string](), 1, "1", "2"))) + is.Equal([]string{"1", "2", "0"}, slices.Collect(Splice(values("0"), 0, "1", "2"))) + is.Equal([]string{"0", "1", "2"}, slices.Collect(Splice(values("0"), 1, "1", "2"))) + + // type preserved + type myStrings iter.Seq[string] + allStrings := myStrings(values("", "foo", "bar")) + nonempty := Splice(allStrings, 1, "1", "2") + is.IsType(nonempty, allStrings, "type preserved") +} + +func TestCutPrefix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual, result := CutPrefix(values("a", "a", "b"), []string{"a"}) + is.True(result) + is.Equal([]string{"a", "b"}, slices.Collect(actual)) + + actual, result = CutPrefix(values("a", "a", "b"), []string{"a"}) + is.True(result) + is.Equal([]string{"a", "b"}, slices.Collect(actual)) + + actual, result = CutPrefix(values("a", "a", "b"), []string{"b"}) + is.False(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutPrefix(values[string](), []string{"b"}) + is.False(result) + is.Empty(slices.Collect(actual)) + + actual, result = CutPrefix(values("a", "a", "b"), []string{}) + is.True(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutPrefix(values("a", "a", "b"), []string{"a", "a", "b", "b"}) + is.False(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutPrefix(values("a", "a", "b"), []string{"a", "b"}) + is.False(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) +} + +func TestCutSuffix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual, result := CutSuffix(values("a", "a", "b"), []string{"c"}) + is.False(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutSuffix(values("a", "a", "b"), []string{"b"}) + is.True(result) + is.Equal([]string{"a", "a"}, slices.Collect(actual)) + + actual, result = CutSuffix(values("a", "a", "b"), []string{}) + is.True(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutSuffix(values("a", "a", "b"), []string{"a"}) + is.False(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) + + actual, result = CutSuffix(values("a", "a", "b"), []string{}) + is.True(result) + is.Equal([]string{"a", "a", "b"}, slices.Collect(actual)) +} + +func TestTrim(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual := Trim(values("a", "b", "c", "d", "e", "f", "g"), "a", "b") + is.Equal([]string{"c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = Trim(values("a", "b", "c", "d", "e", "f", "g"), "g", "f") + is.Equal([]string{"a", "b", "c", "d", "e"}, slices.Collect(actual)) + actual = Trim(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g") + is.Empty(slices.Collect(actual)) + actual = Trim(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g", "h") + is.Empty(slices.Collect(actual)) + actual = Trim(values("a", "b", "c", "d", "e", "f", "g")) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) +} + +func TestTrimFirst(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual := TrimFirst(values("a", "a", "b", "c", "d", "e", "f", "g"), "a", "b") + is.Equal([]string{"c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimFirst(values("a", "b", "c", "d", "e", "f", "g"), "b", "a") + is.Equal([]string{"c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimFirst(values("a", "b", "c", "d", "e", "f", "g"), "g", "f") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimFirst(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g") + is.Empty(slices.Collect(actual)) + actual = TrimFirst(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g", "h") + is.Empty(slices.Collect(actual)) + actual = TrimFirst(values("a", "b", "c", "d", "e", "f", "g")) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) +} + +func TestTrimPrefix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual := TrimPrefix(values("a", "b", "a", "b", "c", "d", "e", "f", "g"), []string{"a", "b"}) + is.Equal([]string{"c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimPrefix(values("a", "b", "c", "d", "e", "f", "g"), []string{"b", "a"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimPrefix(values("a", "b", "c", "d", "e", "f", "g"), []string{"g", "f"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimPrefix(values("a", "b", "c", "d", "e", "f", "g"), []string{"a", "b", "c", "d", "e", "f", "g"}) + is.Empty(slices.Collect(actual)) + actual = TrimPrefix(values("a", "b", "c", "d", "e", "f", "g"), []string{"a", "b", "c", "d", "e", "f", "g", "h"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimPrefix(values("a", "b", "c", "d", "e", "f", "g"), []string{}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) +} + +func TestTrimLast(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual := TrimLast(values("a", "b", "c", "d", "e", "f", "g"), "a", "b") + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimLast(values("a", "b", "c", "d", "e", "f", "g", "g"), "g", "f") + is.Equal([]string{"a", "b", "c", "d", "e"}, slices.Collect(actual)) + actual = TrimLast(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g") + is.Empty(slices.Collect(actual)) + actual = TrimLast(values("a", "b", "c", "d", "e", "f", "g"), "a", "b", "c", "d", "e", "f", "g", "h") + is.Empty(slices.Collect(actual)) + actual = TrimLast(values("a", "b", "c", "d", "e", "f", "g")) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) +} + +func TestTrimSuffix(t *testing.T) { + t.Parallel() + is := assert.New(t) + + actual := TrimSuffix(values("a", "b", "c", "d", "e", "f", "g"), []string{"a", "b"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "g", "f", "g"), []string{"f", "g"}) + is.Equal([]string{"a", "b", "c", "d", "e"}, slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "g", "f", "g"), []string{"g", "f"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g", "f", "g"}, slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "f", "g"), []string{"f", "g"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f"}, slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "g"), []string{"a", "b", "c", "d", "e", "f", "g"}) + is.Empty(slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "g"), []string{"a", "b", "c", "d", "e", "f", "g", "h"}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) + actual = TrimSuffix(values("a", "b", "c", "d", "e", "f", "g"), []string{}) + is.Equal([]string{"a", "b", "c", "d", "e", "f", "g"}, slices.Collect(actual)) +} diff --git a/it/string.go b/it/string.go new file mode 100644 index 0000000..50267e1 --- /dev/null +++ b/it/string.go @@ -0,0 +1,34 @@ +//go:build go1.23 + +package it + +import "iter" + +// ChunkString returns a sequence of strings split into groups of length size. If the string can't be split evenly, +// the final chunk will be the remaining characters. +func ChunkString[T ~string](str T, size int) iter.Seq[T] { + if size <= 0 { + panic("it.ChunkString: size must be greater than 0") + } + + return func(yield func(T) bool) { + if len(str) == 0 || size >= len(str) { + yield(str) + return + } + + currentLen := 0 + currentStart := 0 + for i := range str { + if currentLen == size { + if !yield(str[currentStart:i]) { + return + } + currentLen = 0 + currentStart = i + } + currentLen++ + } + yield(str[currentStart:]) + } +} diff --git a/it/string_example_test.go b/it/string_example_test.go new file mode 100644 index 0000000..9daed75 --- /dev/null +++ b/it/string_example_test.go @@ -0,0 +1,25 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "slices" +) + +func ExampleChunkString() { + result1 := ChunkString("123456", 2) + result2 := ChunkString("1234567", 2) + result3 := ChunkString("", 2) + result4 := ChunkString("1", 2) + + fmt.Printf("%v\n", slices.Collect(result1)) + fmt.Printf("%v\n", slices.Collect(result2)) + fmt.Printf("%v\n", slices.Collect(result3)) + fmt.Printf("%v\n", slices.Collect(result4)) + // Output: + // [12 34 56] + // [12 34 56 7] + // [] + // [1] +} diff --git a/it/string_test.go b/it/string_test.go new file mode 100644 index 0000000..0bfb3c1 --- /dev/null +++ b/it/string_test.go @@ -0,0 +1,37 @@ +//go:build go1.23 + +package it + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestChunkString(t *testing.T) { + t.Parallel() + is := assert.New(t) + + result1 := ChunkString("12345", 2) + is.Equal([]string{"12", "34", "5"}, slices.Collect(result1)) + + result2 := ChunkString("123456", 2) + is.Equal([]string{"12", "34", "56"}, slices.Collect(result2)) + + result3 := ChunkString("123456", 6) + is.Equal([]string{"123456"}, slices.Collect(result3)) + + result4 := ChunkString("123456", 10) + is.Equal([]string{"123456"}, slices.Collect(result4)) + + result5 := ChunkString("", 2) + is.Equal([]string{""}, slices.Collect(result5)) + + result6 := ChunkString("明1好休2林森", 2) + is.Equal([]string{"明1", "好休", "2林", "森"}, slices.Collect(result6)) + + is.PanicsWithValue("it.ChunkString: size must be greater than 0", func() { + ChunkString("12345", 0) + }) +} diff --git a/it/tuples.go b/it/tuples.go new file mode 100644 index 0000000..20e4762 --- /dev/null +++ b/it/tuples.go @@ -0,0 +1,605 @@ +//go:build go1.23 + +package it + +import ( + "iter" + + "github.com/samber/lo" +) + +// Zip2 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip2[A, B any](a iter.Seq[A], b iter.Seq[B]) iter.Seq[lo.Tuple2[A, B]] { + return func(yield func(lo.Tuple2[A, B]) bool) { + var next lo.Tuple2[func() (A, bool), func() (B, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + + for { + var item lo.Tuple2[A, B] + var ok [2]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + if ok == [2]bool{} { + return + } + yield(item) + } + } +} + +// Zip3 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip3[A, B, C any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C]) iter.Seq[lo.Tuple3[A, B, C]] { + return func(yield func(lo.Tuple3[A, B, C]) bool) { + var next lo.Tuple3[func() (A, bool), func() (B, bool), func() (C, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + + for { + var item lo.Tuple3[A, B, C] + var ok [3]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + if ok == [3]bool{} { + return + } + yield(item) + } + } +} + +// Zip4 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip4[A, B, C, D any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D]) iter.Seq[lo.Tuple4[A, B, C, D]] { + return func(yield func(lo.Tuple4[A, B, C, D]) bool) { + var next lo.Tuple4[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + + for { + var item lo.Tuple4[A, B, C, D] + var ok [4]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + if ok == [4]bool{} { + return + } + yield(item) + } + } +} + +// Zip5 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip5[A, B, C, D, E any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E]) iter.Seq[lo.Tuple5[A, B, C, D, E]] { + return func(yield func(lo.Tuple5[A, B, C, D, E]) bool) { + var next lo.Tuple5[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool), func() (E, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + next.E, stop = iter.Pull(e) + defer stop() + + for { + var item lo.Tuple5[A, B, C, D, E] + var ok [5]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + item.E, ok[4] = next.E() + if ok == [5]bool{} { + return + } + yield(item) + } + } +} + +// Zip6 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip6[A, B, C, D, E, F any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F]) iter.Seq[lo.Tuple6[A, B, C, D, E, F]] { + return func(yield func(lo.Tuple6[A, B, C, D, E, F]) bool) { + var next lo.Tuple6[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool), func() (E, bool), func() (F, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + next.E, stop = iter.Pull(e) + defer stop() + next.F, stop = iter.Pull(f) + defer stop() + + for { + var item lo.Tuple6[A, B, C, D, E, F] + var ok [6]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + item.E, ok[4] = next.E() + item.F, ok[5] = next.F() + if ok == [6]bool{} { + return + } + yield(item) + } + } +} + +// Zip7 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip7[A, B, C, D, E, F, G any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G]) iter.Seq[lo.Tuple7[A, B, C, D, E, F, G]] { + return func(yield func(lo.Tuple7[A, B, C, D, E, F, G]) bool) { + var next lo.Tuple7[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool), func() (E, bool), func() (F, bool), func() (G, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + next.E, stop = iter.Pull(e) + defer stop() + next.F, stop = iter.Pull(f) + defer stop() + next.G, stop = iter.Pull(g) + defer stop() + + for { + var item lo.Tuple7[A, B, C, D, E, F, G] + var ok [7]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + item.E, ok[4] = next.E() + item.F, ok[5] = next.F() + item.G, ok[6] = next.G() + if ok == [7]bool{} { + return + } + yield(item) + } + } +} + +// Zip8 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip8[A, B, C, D, E, F, G, H any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G], h iter.Seq[H]) iter.Seq[lo.Tuple8[A, B, C, D, E, F, G, H]] { + return func(yield func(lo.Tuple8[A, B, C, D, E, F, G, H]) bool) { + var next lo.Tuple8[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool), func() (E, bool), func() (F, bool), func() (G, bool), func() (H, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + next.E, stop = iter.Pull(e) + defer stop() + next.F, stop = iter.Pull(f) + defer stop() + next.G, stop = iter.Pull(g) + defer stop() + next.H, stop = iter.Pull(h) + defer stop() + + for { + var item lo.Tuple8[A, B, C, D, E, F, G, H] + var ok [8]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + item.E, ok[4] = next.E() + item.F, ok[5] = next.F() + item.G, ok[6] = next.G() + item.H, ok[7] = next.H() + if ok == [8]bool{} { + return + } + yield(item) + } + } +} + +// Zip9 creates a sequence of grouped elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func Zip9[A, B, C, D, E, F, G, H, I any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G], h iter.Seq[H], i iter.Seq[I]) iter.Seq[lo.Tuple9[A, B, C, D, E, F, G, H, I]] { + return func(yield func(lo.Tuple9[A, B, C, D, E, F, G, H, I]) bool) { + var next lo.Tuple9[func() (A, bool), func() (B, bool), func() (C, bool), func() (D, bool), func() (E, bool), func() (F, bool), func() (G, bool), func() (H, bool), func() (I, bool)] + var stop func() + next.A, stop = iter.Pull(a) + defer stop() + next.B, stop = iter.Pull(b) + defer stop() + next.C, stop = iter.Pull(c) + defer stop() + next.D, stop = iter.Pull(d) + defer stop() + next.E, stop = iter.Pull(e) + defer stop() + next.F, stop = iter.Pull(f) + defer stop() + next.G, stop = iter.Pull(g) + defer stop() + next.H, stop = iter.Pull(h) + defer stop() + next.I, stop = iter.Pull(i) + defer stop() + + for { + var item lo.Tuple9[A, B, C, D, E, F, G, H, I] + var ok [9]bool + item.A, ok[0] = next.A() + item.B, ok[1] = next.B() + item.C, ok[2] = next.C() + item.D, ok[3] = next.D() + item.E, ok[4] = next.E() + item.F, ok[5] = next.F() + item.G, ok[6] = next.G() + item.H, ok[7] = next.H() + item.I, ok[8] = next.I() + if ok == [9]bool{} { + return + } + yield(item) + } + } +} + +// ZipBy2 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy2[A, B, Out any](a iter.Seq[A], b iter.Seq[B], transform func(a A, b B) Out) iter.Seq[Out] { + return Map(Zip2(a, b), func(item lo.Tuple2[A, B]) Out { + return transform(item.A, item.B) + }) +} + +// ZipBy3 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy3[A, B, C, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], transform func(a A, b B, c C) Out) iter.Seq[Out] { + return Map(Zip3(a, b, c), func(item lo.Tuple3[A, B, C]) Out { + return transform(item.A, item.B, item.C) + }) +} + +// ZipBy4 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy4[A, B, C, D, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], transform func(a A, b B, c C, d D) Out) iter.Seq[Out] { + return Map(Zip4(a, b, c, d), func(item lo.Tuple4[A, B, C, D]) Out { + return transform(item.A, item.B, item.C, item.D) + }) +} + +// ZipBy5 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy5[A, B, C, D, E, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], transform func(a A, b B, c C, d D, e E) Out) iter.Seq[Out] { + return Map(Zip5(a, b, c, d, e), func(item lo.Tuple5[A, B, C, D, E]) Out { + return transform(item.A, item.B, item.C, item.D, item.E) + }) +} + +// ZipBy6 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy6[A, B, C, D, E, F, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], transform func(a A, b B, c C, d D, e E, f F) Out) iter.Seq[Out] { + return Map(Zip6(a, b, c, d, e, f), func(item lo.Tuple6[A, B, C, D, E, F]) Out { + return transform(item.A, item.B, item.C, item.D, item.E, item.F) + }) +} + +// ZipBy7 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy7[A, B, C, D, E, F, G, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G], transform func(a A, b B, c C, d D, e E, f F, g G) Out) iter.Seq[Out] { + return Map(Zip7(a, b, c, d, e, f, g), func(item lo.Tuple7[A, B, C, D, E, F, G]) Out { + return transform(item.A, item.B, item.C, item.D, item.E, item.F, item.G) + }) +} + +// ZipBy8 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy8[A, B, C, D, E, F, G, H, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G], h iter.Seq[H], transform func(a A, b B, c C, d D, e E, f F, g G, h H) Out) iter.Seq[Out] { + return Map(Zip8(a, b, c, d, e, f, g, h), func(item lo.Tuple8[A, B, C, D, E, F, G, H]) Out { + return transform(item.A, item.B, item.C, item.D, item.E, item.F, item.G, item.H) + }) +} + +// ZipBy9 creates a sequence of transformed elements, the first of which contains the first elements +// of the given sequences, the second of which contains the second elements of the given sequences, and so on. +// When collections are different sizes, the Tuple attributes are filled with zero value. +func ZipBy9[A, B, C, D, E, F, G, H, I, Out any](a iter.Seq[A], b iter.Seq[B], c iter.Seq[C], d iter.Seq[D], e iter.Seq[E], f iter.Seq[F], g iter.Seq[G], h iter.Seq[H], i iter.Seq[I], transform func(a A, b B, c C, d D, e E, f F, g G, h H, i I) Out) iter.Seq[Out] { + return Map(Zip9(a, b, c, d, e, f, g, h, i), func(item lo.Tuple9[A, B, C, D, E, F, G, H, I]) Out { + return transform(item.A, item.B, item.C, item.D, item.E, item.F, item.G, item.H, item.I) + }) +} + +// CrossJoin2 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin2[A, B any](listA iter.Seq[A], listB iter.Seq[B]) iter.Seq[lo.Tuple2[A, B]] { + return CrossJoinBy2(listA, listB, lo.T2[A, B]) +} + +// CrossJoin3 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin3[A, B, C any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C]) iter.Seq[lo.Tuple3[A, B, C]] { + return CrossJoinBy3(listA, listB, listC, lo.T3[A, B, C]) +} + +// CrossJoin4 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin4[A, B, C, D any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D]) iter.Seq[lo.Tuple4[A, B, C, D]] { + return CrossJoinBy4(listA, listB, listC, listD, lo.T4[A, B, C, D]) +} + +// CrossJoin5 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin5[A, B, C, D, E any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E]) iter.Seq[lo.Tuple5[A, B, C, D, E]] { + return CrossJoinBy5(listA, listB, listC, listD, listE, lo.T5[A, B, C, D, E]) +} + +// CrossJoin6 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin6[A, B, C, D, E, F any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F]) iter.Seq[lo.Tuple6[A, B, C, D, E, F]] { + return CrossJoinBy6(listA, listB, listC, listD, listE, listF, lo.T6[A, B, C, D, E, F]) +} + +// CrossJoin7 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin7[A, B, C, D, E, F, G any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G]) iter.Seq[lo.Tuple7[A, B, C, D, E, F, G]] { + return CrossJoinBy7(listA, listB, listC, listD, listE, listF, listG, lo.T7[A, B, C, D, E, F, G]) +} + +// CrossJoin8 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin8[A, B, C, D, E, F, G, H any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G], listH iter.Seq[H]) iter.Seq[lo.Tuple8[A, B, C, D, E, F, G, H]] { + return CrossJoinBy8(listA, listB, listC, listD, listE, listF, listG, listH, lo.T8[A, B, C, D, E, F, G, H]) +} + +// CrossJoin9 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. +// Returns an empty list if a list is empty. +func CrossJoin9[A, B, C, D, E, F, G, H, I any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G], listH iter.Seq[H], listI iter.Seq[I]) iter.Seq[lo.Tuple9[A, B, C, D, E, F, G, H, I]] { + return CrossJoinBy9(listA, listB, listC, listD, listE, listF, listG, listH, listI, lo.T9[A, B, C, D, E, F, G, H, I]) +} + +// CrossJoinBy2 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy2[A, B, Out any](listA iter.Seq[A], listB iter.Seq[B], project func(a A, b B) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + if !yield(project(a, b)) { + return + } + } + } + } +} + +// CrossJoinBy3 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy3[A, B, C, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], project func(a A, b B, c C) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + if !yield(project(a, b, c)) { + return + } + } + } + } + } +} + +// CrossJoinBy4 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy4[A, B, C, D, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], project func(a A, b B, c C, d D) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + if !yield(project(a, b, c, d)) { + return + } + } + } + } + } + } +} + +// CrossJoinBy5 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy5[A, B, C, D, E, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], project func(a A, b B, c C, d D, e E) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + for e := range listE { + if !yield(project(a, b, c, d, e)) { + return + } + } + } + } + } + } + } +} + +// CrossJoinBy6 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy6[A, B, C, D, E, F, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], project func(a A, b B, c C, d D, e E, f F) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + for e := range listE { + for f := range listF { + if !yield(project(a, b, c, d, e, f)) { + return + } + } + } + } + } + } + } + } +} + +// CrossJoinBy7 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy7[A, B, C, D, E, F, G, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G], project func(a A, b B, c C, d D, e E, f F, g G) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + for e := range listE { + for f := range listF { + for g := range listG { + if !yield(project(a, b, c, d, e, f, g)) { + return + } + } + } + } + } + } + } + } + } +} + +// CrossJoinBy8 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy8[A, B, C, D, E, F, G, H, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G], listH iter.Seq[H], project func(a A, b B, c C, d D, e E, f F, g G, h H) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + for e := range listE { + for f := range listF { + for g := range listG { + for h := range listH { + if !yield(project(a, b, c, d, e, f, g, h)) { + return + } + } + } + } + } + } + } + } + } + } +} + +// CrossJoinBy9 combines every item from one list with every item from others. +// It is the cartesian product of lists received as arguments. The project function +// is used to create the output values. +// Returns an empty list if a list is empty. +func CrossJoinBy9[A, B, C, D, E, F, G, H, I, Out any](listA iter.Seq[A], listB iter.Seq[B], listC iter.Seq[C], listD iter.Seq[D], listE iter.Seq[E], listF iter.Seq[F], listG iter.Seq[G], listH iter.Seq[H], listI iter.Seq[I], project func(a A, b B, c C, d D, e E, f F, g G, h H, i I) Out) iter.Seq[Out] { + return func(yield func(Out) bool) { + for a := range listA { + for b := range listB { + for c := range listC { + for d := range listD { + for e := range listE { + for f := range listF { + for g := range listG { + for h := range listH { + for i := range listI { + if !yield(project(a, b, c, d, e, f, g, h, i)) { + return + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/it/tuples_example_test.go b/it/tuples_example_test.go new file mode 100644 index 0000000..98b9a14 --- /dev/null +++ b/it/tuples_example_test.go @@ -0,0 +1,440 @@ +//go:build go1.23 + +package it + +import ( + "fmt" + "slices" +) + +func ExampleZip2() { + result := Zip2(values("hello"), values(2)) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2}] +} + +func ExampleZip3() { + result := Zip3(values("hello"), values(2), values(true)) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true}] +} + +func ExampleZip4() { + result := Zip4(values("hello"), values(2), values(true), values(foo{bar: "bar"})) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar}}] +} + +func ExampleZip5() { + result := Zip5(values("hello"), values(2), values(true), values(foo{bar: "bar"}), values(4.2)) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar} 4.2}] +} + +func ExampleZip6() { + result := Zip6(values("hello"), values(2), values(true), values(foo{bar: "bar"}), values(4.2), values("plop")) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar} 4.2 plop}] +} + +func ExampleZip7() { + result := Zip7(values("hello"), values(2), values(true), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false)) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar} 4.2 plop false}] +} + +func ExampleZip8() { + result := Zip8(values("hello"), values(2), values(true), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42)) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar} 4.2 plop false 42}] +} + +func ExampleZip9() { + result := Zip9(values("hello"), values(2), values(true), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42), values("hello world")) + fmt.Printf("%v", slices.Collect(result)) + // Output: [{hello 2 true {bar} 4.2 plop false 42 hello world}] +} + +func ExampleCrossJoin2() { + result := CrossJoin2(values("a", "b"), values(1, 2, 3, 4)) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1} + // {a 2} + // {a 3} + // {a 4} + // {b 1} + // {b 2} + // {b 3} + // {b 4} +} + +func ExampleCrossJoin3() { + result := CrossJoin3(values("a", "b"), values(1, 2, 3, 4), values(true, false)) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true} + // {a 1 false} + // {a 2 true} + // {a 2 false} + // {a 3 true} + // {a 3 false} + // {a 4 true} + // {a 4 false} + // {b 1 true} + // {b 1 false} + // {b 2 true} + // {b 2 false} + // {b 3 true} + // {b 3 false} + // {b 4 true} + // {b 4 false} +} + +func ExampleCrossJoin4() { + result := CrossJoin4(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"})) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar}} + // {a 1 false {bar}} + // {a 2 true {bar}} + // {a 2 false {bar}} + // {a 3 true {bar}} + // {a 3 false {bar}} + // {a 4 true {bar}} + // {a 4 false {bar}} + // {b 1 true {bar}} + // {b 1 false {bar}} + // {b 2 true {bar}} + // {b 2 false {bar}} + // {b 3 true {bar}} + // {b 3 false {bar}} + // {b 4 true {bar}} + // {b 4 false {bar}} +} + +func ExampleCrossJoin5() { + result := CrossJoin5(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2)) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar} 4.2} + // {a 1 false {bar} 4.2} + // {a 2 true {bar} 4.2} + // {a 2 false {bar} 4.2} + // {a 3 true {bar} 4.2} + // {a 3 false {bar} 4.2} + // {a 4 true {bar} 4.2} + // {a 4 false {bar} 4.2} + // {b 1 true {bar} 4.2} + // {b 1 false {bar} 4.2} + // {b 2 true {bar} 4.2} + // {b 2 false {bar} 4.2} + // {b 3 true {bar} 4.2} + // {b 3 false {bar} 4.2} + // {b 4 true {bar} 4.2} + // {b 4 false {bar} 4.2} +} + +func ExampleCrossJoin6() { + result := CrossJoin6(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop")) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar} 4.2 plop} + // {a 1 false {bar} 4.2 plop} + // {a 2 true {bar} 4.2 plop} + // {a 2 false {bar} 4.2 plop} + // {a 3 true {bar} 4.2 plop} + // {a 3 false {bar} 4.2 plop} + // {a 4 true {bar} 4.2 plop} + // {a 4 false {bar} 4.2 plop} + // {b 1 true {bar} 4.2 plop} + // {b 1 false {bar} 4.2 plop} + // {b 2 true {bar} 4.2 plop} + // {b 2 false {bar} 4.2 plop} + // {b 3 true {bar} 4.2 plop} + // {b 3 false {bar} 4.2 plop} + // {b 4 true {bar} 4.2 plop} + // {b 4 false {bar} 4.2 plop} +} + +func ExampleCrossJoin7() { + result := CrossJoin7(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false)) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar} 4.2 plop false} + // {a 1 false {bar} 4.2 plop false} + // {a 2 true {bar} 4.2 plop false} + // {a 2 false {bar} 4.2 plop false} + // {a 3 true {bar} 4.2 plop false} + // {a 3 false {bar} 4.2 plop false} + // {a 4 true {bar} 4.2 plop false} + // {a 4 false {bar} 4.2 plop false} + // {b 1 true {bar} 4.2 plop false} + // {b 1 false {bar} 4.2 plop false} + // {b 2 true {bar} 4.2 plop false} + // {b 2 false {bar} 4.2 plop false} + // {b 3 true {bar} 4.2 plop false} + // {b 3 false {bar} 4.2 plop false} + // {b 4 true {bar} 4.2 plop false} + // {b 4 false {bar} 4.2 plop false} +} + +func ExampleCrossJoin8() { + result := CrossJoin8(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42)) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar} 4.2 plop false 42} + // {a 1 false {bar} 4.2 plop false 42} + // {a 2 true {bar} 4.2 plop false 42} + // {a 2 false {bar} 4.2 plop false 42} + // {a 3 true {bar} 4.2 plop false 42} + // {a 3 false {bar} 4.2 plop false 42} + // {a 4 true {bar} 4.2 plop false 42} + // {a 4 false {bar} 4.2 plop false 42} + // {b 1 true {bar} 4.2 plop false 42} + // {b 1 false {bar} 4.2 plop false 42} + // {b 2 true {bar} 4.2 plop false 42} + // {b 2 false {bar} 4.2 plop false 42} + // {b 3 true {bar} 4.2 plop false 42} + // {b 3 false {bar} 4.2 plop false 42} + // {b 4 true {bar} 4.2 plop false 42} + // {b 4 false {bar} 4.2 plop false 42} +} + +func ExampleCrossJoin9() { + result := CrossJoin9(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42), values("hello world")) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // {a 1 true {bar} 4.2 plop false 42 hello world} + // {a 1 false {bar} 4.2 plop false 42 hello world} + // {a 2 true {bar} 4.2 plop false 42 hello world} + // {a 2 false {bar} 4.2 plop false 42 hello world} + // {a 3 true {bar} 4.2 plop false 42 hello world} + // {a 3 false {bar} 4.2 plop false 42 hello world} + // {a 4 true {bar} 4.2 plop false 42 hello world} + // {a 4 false {bar} 4.2 plop false 42 hello world} + // {b 1 true {bar} 4.2 plop false 42 hello world} + // {b 1 false {bar} 4.2 plop false 42 hello world} + // {b 2 true {bar} 4.2 plop false 42 hello world} + // {b 2 false {bar} 4.2 plop false 42 hello world} + // {b 3 true {bar} 4.2 plop false 42 hello world} + // {b 3 false {bar} 4.2 plop false 42 hello world} + // {b 4 true {bar} 4.2 plop false 42 hello world} + // {b 4 false {bar} 4.2 plop false 42 hello world} +} + +func ExampleCrossJoinBy2() { + result := CrossJoinBy2(values("a", "b"), values(1, 2, 3, 4), func(a string, b int) string { + return fmt.Sprintf("%v-%v", a, b) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1 + // a-2 + // a-3 + // a-4 + // b-1 + // b-2 + // b-3 + // b-4 +} + +func ExampleCrossJoinBy3() { + result := CrossJoinBy3(values("a", "b"), values(1, 2, 3, 4), values(true, false), func(a string, b int, c bool) string { + return fmt.Sprintf("%v-%v-%v", a, b, c) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true + // a-1-false + // a-2-true + // a-2-false + // a-3-true + // a-3-false + // a-4-true + // a-4-false + // b-1-true + // b-1-false + // b-2-true + // b-2-false + // b-3-true + // b-3-false + // b-4-true + // b-4-false +} + +func ExampleCrossJoinBy4() { + result := CrossJoinBy4(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), func(a string, b int, c bool, d foo) string { + return fmt.Sprintf("%v-%v-%v-%v", a, b, c, d) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar} + // a-1-false-{bar} + // a-2-true-{bar} + // a-2-false-{bar} + // a-3-true-{bar} + // a-3-false-{bar} + // a-4-true-{bar} + // a-4-false-{bar} + // b-1-true-{bar} + // b-1-false-{bar} + // b-2-true-{bar} + // b-2-false-{bar} + // b-3-true-{bar} + // b-3-false-{bar} + // b-4-true-{bar} + // b-4-false-{bar} +} + +func ExampleCrossJoinBy5() { + result := CrossJoinBy5(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), func(a string, b int, c bool, d foo, e float64) string { + return fmt.Sprintf("%v-%v-%v-%v-%v", a, b, c, d, e) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar}-4.2 + // a-1-false-{bar}-4.2 + // a-2-true-{bar}-4.2 + // a-2-false-{bar}-4.2 + // a-3-true-{bar}-4.2 + // a-3-false-{bar}-4.2 + // a-4-true-{bar}-4.2 + // a-4-false-{bar}-4.2 + // b-1-true-{bar}-4.2 + // b-1-false-{bar}-4.2 + // b-2-true-{bar}-4.2 + // b-2-false-{bar}-4.2 + // b-3-true-{bar}-4.2 + // b-3-false-{bar}-4.2 + // b-4-true-{bar}-4.2 + // b-4-false-{bar}-4.2 +} + +func ExampleCrossJoinBy6() { + result := CrossJoinBy6(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), func(a string, b int, c bool, d foo, e float64, f string) string { + return fmt.Sprintf("%v-%v-%v-%v-%v-%v", a, b, c, d, e, f) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar}-4.2-plop + // a-1-false-{bar}-4.2-plop + // a-2-true-{bar}-4.2-plop + // a-2-false-{bar}-4.2-plop + // a-3-true-{bar}-4.2-plop + // a-3-false-{bar}-4.2-plop + // a-4-true-{bar}-4.2-plop + // a-4-false-{bar}-4.2-plop + // b-1-true-{bar}-4.2-plop + // b-1-false-{bar}-4.2-plop + // b-2-true-{bar}-4.2-plop + // b-2-false-{bar}-4.2-plop + // b-3-true-{bar}-4.2-plop + // b-3-false-{bar}-4.2-plop + // b-4-true-{bar}-4.2-plop + // b-4-false-{bar}-4.2-plop +} + +func ExampleCrossJoinBy7() { + result := CrossJoinBy7(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), func(a string, b int, c bool, d foo, e float64, f string, g bool) string { + return fmt.Sprintf("%v-%v-%v-%v-%v-%v-%v", a, b, c, d, e, f, g) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar}-4.2-plop-false + // a-1-false-{bar}-4.2-plop-false + // a-2-true-{bar}-4.2-plop-false + // a-2-false-{bar}-4.2-plop-false + // a-3-true-{bar}-4.2-plop-false + // a-3-false-{bar}-4.2-plop-false + // a-4-true-{bar}-4.2-plop-false + // a-4-false-{bar}-4.2-plop-false + // b-1-true-{bar}-4.2-plop-false + // b-1-false-{bar}-4.2-plop-false + // b-2-true-{bar}-4.2-plop-false + // b-2-false-{bar}-4.2-plop-false + // b-3-true-{bar}-4.2-plop-false + // b-3-false-{bar}-4.2-plop-false + // b-4-true-{bar}-4.2-plop-false + // b-4-false-{bar}-4.2-plop-false +} + +func ExampleCrossJoinBy8() { + result := CrossJoinBy8(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42), func(a string, b int, c bool, d foo, e float64, f string, g bool, h int) string { + return fmt.Sprintf("%v-%v-%v-%v-%v-%v-%v-%v", a, b, c, d, e, f, g, h) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar}-4.2-plop-false-42 + // a-1-false-{bar}-4.2-plop-false-42 + // a-2-true-{bar}-4.2-plop-false-42 + // a-2-false-{bar}-4.2-plop-false-42 + // a-3-true-{bar}-4.2-plop-false-42 + // a-3-false-{bar}-4.2-plop-false-42 + // a-4-true-{bar}-4.2-plop-false-42 + // a-4-false-{bar}-4.2-plop-false-42 + // b-1-true-{bar}-4.2-plop-false-42 + // b-1-false-{bar}-4.2-plop-false-42 + // b-2-true-{bar}-4.2-plop-false-42 + // b-2-false-{bar}-4.2-plop-false-42 + // b-3-true-{bar}-4.2-plop-false-42 + // b-3-false-{bar}-4.2-plop-false-42 + // b-4-true-{bar}-4.2-plop-false-42 + // b-4-false-{bar}-4.2-plop-false-42 +} + +func ExampleCrossJoinBy9() { + result := CrossJoinBy9(values("a", "b"), values(1, 2, 3, 4), values(true, false), values(foo{bar: "bar"}), values(4.2), values("plop"), values(false), values(42), values("hello world"), func(a string, b int, c bool, d foo, e float64, f string, g bool, h int, i string) string { + return fmt.Sprintf("%v-%v-%v-%v-%v-%v-%v-%v-%v", a, b, c, d, e, f, g, h, i) + }) + for r := range result { + fmt.Printf("%v\n", r) + } + // Output: + // a-1-true-{bar}-4.2-plop-false-42-hello world + // a-1-false-{bar}-4.2-plop-false-42-hello world + // a-2-true-{bar}-4.2-plop-false-42-hello world + // a-2-false-{bar}-4.2-plop-false-42-hello world + // a-3-true-{bar}-4.2-plop-false-42-hello world + // a-3-false-{bar}-4.2-plop-false-42-hello world + // a-4-true-{bar}-4.2-plop-false-42-hello world + // a-4-false-{bar}-4.2-plop-false-42-hello world + // b-1-true-{bar}-4.2-plop-false-42-hello world + // b-1-false-{bar}-4.2-plop-false-42-hello world + // b-2-true-{bar}-4.2-plop-false-42-hello world + // b-2-false-{bar}-4.2-plop-false-42-hello world + // b-3-true-{bar}-4.2-plop-false-42-hello world + // b-3-false-{bar}-4.2-plop-false-42-hello world + // b-4-true-{bar}-4.2-plop-false-42-hello world + // b-4-false-{bar}-4.2-plop-false-42-hello world +} diff --git a/it/tuples_test.go b/it/tuples_test.go new file mode 100644 index 0000000..0a65c8c --- /dev/null +++ b/it/tuples_test.go @@ -0,0 +1,363 @@ +//go:build go1.23 + +package it + +import ( + "slices" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" +) + +func TestZip(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := Zip2( + values("a", "b"), + values(1, 2), + ) + + r2 := Zip3( + values("a", "b", "c"), + values(1, 2, 3), + values(4, 5, 6), + ) + + r3 := Zip4( + values("a", "b", "c", "d"), + values(1, 2, 3, 4), + values(5, 6, 7, 8), + values(true, true, true, true), + ) + + r4 := Zip5( + values("a", "b", "c", "d", "e"), + values(1, 2, 3, 4, 5), + values(6, 7, 8, 9, 10), + values(true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5), + ) + + r5 := Zip6( + values("a", "b", "c", "d", "e", "f"), + values(1, 2, 3, 4, 5, 6), + values(7, 8, 9, 10, 11, 12), + values(true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06), + ) + + r6 := Zip7( + values("a", "b", "c", "d", "e", "f", "g"), + values(1, 2, 3, 4, 5, 6, 7), + values(8, 9, 10, 11, 12, 13, 14), + values(true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07), + values[int8](1, 2, 3, 4, 5, 6, 7), + ) + + r7 := Zip8( + values("a", "b", "c", "d", "e", "f", "g", "h"), + values(1, 2, 3, 4, 5, 6, 7, 8), + values(9, 10, 11, 12, 13, 14, 15, 16), + values(true, true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08), + values[int8](1, 2, 3, 4, 5, 6, 7, 8), + values[int16](1, 2, 3, 4, 5, 6, 7, 8), + ) + + r8 := Zip9( + values("a", "b", "c", "d", "e", "f", "g", "h", "i"), + values(1, 2, 3, 4, 5, 6, 7, 8, 9), + values(10, 11, 12, 13, 14, 15, 16, 17, 18), + values(true, true, true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09), + values[int8](1, 2, 3, 4, 5, 6, 7, 8, 9), + values[int16](1, 2, 3, 4, 5, 6, 7, 8, 9), + values[int32](1, 2, 3, 4, 5, 6, 7, 8, 9), + ) + + is.Equal([]lo.Tuple2[string, int]{ + {A: "a", B: 1}, + {A: "b", B: 2}, + }, slices.Collect(r1)) + + is.Equal([]lo.Tuple3[string, int, int]{ + {A: "a", B: 1, C: 4}, + {A: "b", B: 2, C: 5}, + {A: "c", B: 3, C: 6}, + }, slices.Collect(r2)) + + is.Equal([]lo.Tuple4[string, int, int, bool]{ + {A: "a", B: 1, C: 5, D: true}, + {A: "b", B: 2, C: 6, D: true}, + {A: "c", B: 3, C: 7, D: true}, + {A: "d", B: 4, C: 8, D: true}, + }, slices.Collect(r3)) + + is.Equal([]lo.Tuple5[string, int, int, bool, float32]{ + {A: "a", B: 1, C: 6, D: true, E: 0.1}, + {A: "b", B: 2, C: 7, D: true, E: 0.2}, + {A: "c", B: 3, C: 8, D: true, E: 0.3}, + {A: "d", B: 4, C: 9, D: true, E: 0.4}, + {A: "e", B: 5, C: 10, D: true, E: 0.5}, + }, slices.Collect(r4)) + + is.Equal([]lo.Tuple6[string, int, int, bool, float32, float64]{ + {A: "a", B: 1, C: 7, D: true, E: 0.1, F: 0.01}, + {A: "b", B: 2, C: 8, D: true, E: 0.2, F: 0.02}, + {A: "c", B: 3, C: 9, D: true, E: 0.3, F: 0.03}, + {A: "d", B: 4, C: 10, D: true, E: 0.4, F: 0.04}, + {A: "e", B: 5, C: 11, D: true, E: 0.5, F: 0.05}, + {A: "f", B: 6, C: 12, D: true, E: 0.6, F: 0.06}, + }, slices.Collect(r5)) + + is.Equal([]lo.Tuple7[string, int, int, bool, float32, float64, int8]{ + {A: "a", B: 1, C: 8, D: true, E: 0.1, F: 0.01, G: 1}, + {A: "b", B: 2, C: 9, D: true, E: 0.2, F: 0.02, G: 2}, + {A: "c", B: 3, C: 10, D: true, E: 0.3, F: 0.03, G: 3}, + {A: "d", B: 4, C: 11, D: true, E: 0.4, F: 0.04, G: 4}, + {A: "e", B: 5, C: 12, D: true, E: 0.5, F: 0.05, G: 5}, + {A: "f", B: 6, C: 13, D: true, E: 0.6, F: 0.06, G: 6}, + {A: "g", B: 7, C: 14, D: true, E: 0.7, F: 0.07, G: 7}, + }, slices.Collect(r6)) + + is.Equal([]lo.Tuple8[string, int, int, bool, float32, float64, int8, int16]{ + {A: "a", B: 1, C: 9, D: true, E: 0.1, F: 0.01, G: 1, H: 1}, + {A: "b", B: 2, C: 10, D: true, E: 0.2, F: 0.02, G: 2, H: 2}, + {A: "c", B: 3, C: 11, D: true, E: 0.3, F: 0.03, G: 3, H: 3}, + {A: "d", B: 4, C: 12, D: true, E: 0.4, F: 0.04, G: 4, H: 4}, + {A: "e", B: 5, C: 13, D: true, E: 0.5, F: 0.05, G: 5, H: 5}, + {A: "f", B: 6, C: 14, D: true, E: 0.6, F: 0.06, G: 6, H: 6}, + {A: "g", B: 7, C: 15, D: true, E: 0.7, F: 0.07, G: 7, H: 7}, + {A: "h", B: 8, C: 16, D: true, E: 0.8, F: 0.08, G: 8, H: 8}, + }, slices.Collect(r7)) + + is.Equal([]lo.Tuple9[string, int, int, bool, float32, float64, int8, int16, int32]{ + {A: "a", B: 1, C: 10, D: true, E: 0.1, F: 0.01, G: 1, H: 1, I: 1}, + {A: "b", B: 2, C: 11, D: true, E: 0.2, F: 0.02, G: 2, H: 2, I: 2}, + {A: "c", B: 3, C: 12, D: true, E: 0.3, F: 0.03, G: 3, H: 3, I: 3}, + {A: "d", B: 4, C: 13, D: true, E: 0.4, F: 0.04, G: 4, H: 4, I: 4}, + {A: "e", B: 5, C: 14, D: true, E: 0.5, F: 0.05, G: 5, H: 5, I: 5}, + {A: "f", B: 6, C: 15, D: true, E: 0.6, F: 0.06, G: 6, H: 6, I: 6}, + {A: "g", B: 7, C: 16, D: true, E: 0.7, F: 0.07, G: 7, H: 7, I: 7}, + {A: "h", B: 8, C: 17, D: true, E: 0.8, F: 0.08, G: 8, H: 8, I: 8}, + {A: "i", B: 9, C: 18, D: true, E: 0.9, F: 0.09, G: 9, H: 9, I: 9}, + }, slices.Collect(r8)) +} + +func TestZipBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + r1 := ZipBy2( + values("a", "b"), + values(1, 2), + lo.T2[string, int], + ) + + r2 := ZipBy3( + values("a", "b", "c"), + values(1, 2, 3), + values(4, 5, 6), + lo.T3[string, int, int], + ) + + r3 := ZipBy4( + values("a", "b", "c", "d"), + values(1, 2, 3, 4), + values(5, 6, 7, 8), + values(true, true, true, true), + lo.T4[string, int, int, bool], + ) + + r4 := ZipBy5( + values("a", "b", "c", "d", "e"), + values(1, 2, 3, 4, 5), + values(6, 7, 8, 9, 10), + values(true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5), + lo.T5[string, int, int, bool, float32], + ) + + r5 := ZipBy6( + values("a", "b", "c", "d", "e", "f"), + values(1, 2, 3, 4, 5, 6), + values(7, 8, 9, 10, 11, 12), + values(true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06), + lo.T6[string, int, int, bool, float32, float64], + ) + + r6 := ZipBy7( + values("a", "b", "c", "d", "e", "f", "g"), + values(1, 2, 3, 4, 5, 6, 7), + values(8, 9, 10, 11, 12, 13, 14), + values(true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07), + values[int8](1, 2, 3, 4, 5, 6, 7), + lo.T7[string, int, int, bool, float32, float64, int8], + ) + + r7 := ZipBy8( + values("a", "b", "c", "d", "e", "f", "g", "h"), + values(1, 2, 3, 4, 5, 6, 7, 8), + values(9, 10, 11, 12, 13, 14, 15, 16), + values(true, true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08), + values[int8](1, 2, 3, 4, 5, 6, 7, 8), + values[int16](1, 2, 3, 4, 5, 6, 7, 8), + lo.T8[string, int, int, bool, float32, float64, int8, int16], + ) + + r8 := ZipBy9( + values("a", "b", "c", "d", "e", "f", "g", "h", "i"), + values(1, 2, 3, 4, 5, 6, 7, 8, 9), + values(10, 11, 12, 13, 14, 15, 16, 17, 18), + values(true, true, true, true, true, true, true, true, true), + values[float32](0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9), + values(0.01, 0.02, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09), + values[int8](1, 2, 3, 4, 5, 6, 7, 8, 9), + values[int16](1, 2, 3, 4, 5, 6, 7, 8, 9), + values[int32](1, 2, 3, 4, 5, 6, 7, 8, 9), + lo.T9[string, int, int, bool, float32, float64, int8, int16, int32], + ) + + is.Equal([]lo.Tuple2[string, int]{ + {A: "a", B: 1}, + {A: "b", B: 2}, + }, slices.Collect(r1)) + + is.Equal([]lo.Tuple3[string, int, int]{ + {A: "a", B: 1, C: 4}, + {A: "b", B: 2, C: 5}, + {A: "c", B: 3, C: 6}, + }, slices.Collect(r2)) + + is.Equal([]lo.Tuple4[string, int, int, bool]{ + {A: "a", B: 1, C: 5, D: true}, + {A: "b", B: 2, C: 6, D: true}, + {A: "c", B: 3, C: 7, D: true}, + {A: "d", B: 4, C: 8, D: true}, + }, slices.Collect(r3)) + + is.Equal([]lo.Tuple5[string, int, int, bool, float32]{ + {A: "a", B: 1, C: 6, D: true, E: 0.1}, + {A: "b", B: 2, C: 7, D: true, E: 0.2}, + {A: "c", B: 3, C: 8, D: true, E: 0.3}, + {A: "d", B: 4, C: 9, D: true, E: 0.4}, + {A: "e", B: 5, C: 10, D: true, E: 0.5}, + }, slices.Collect(r4)) + + is.Equal([]lo.Tuple6[string, int, int, bool, float32, float64]{ + {A: "a", B: 1, C: 7, D: true, E: 0.1, F: 0.01}, + {A: "b", B: 2, C: 8, D: true, E: 0.2, F: 0.02}, + {A: "c", B: 3, C: 9, D: true, E: 0.3, F: 0.03}, + {A: "d", B: 4, C: 10, D: true, E: 0.4, F: 0.04}, + {A: "e", B: 5, C: 11, D: true, E: 0.5, F: 0.05}, + {A: "f", B: 6, C: 12, D: true, E: 0.6, F: 0.06}, + }, slices.Collect(r5)) + + is.Equal([]lo.Tuple7[string, int, int, bool, float32, float64, int8]{ + {A: "a", B: 1, C: 8, D: true, E: 0.1, F: 0.01, G: 1}, + {A: "b", B: 2, C: 9, D: true, E: 0.2, F: 0.02, G: 2}, + {A: "c", B: 3, C: 10, D: true, E: 0.3, F: 0.03, G: 3}, + {A: "d", B: 4, C: 11, D: true, E: 0.4, F: 0.04, G: 4}, + {A: "e", B: 5, C: 12, D: true, E: 0.5, F: 0.05, G: 5}, + {A: "f", B: 6, C: 13, D: true, E: 0.6, F: 0.06, G: 6}, + {A: "g", B: 7, C: 14, D: true, E: 0.7, F: 0.07, G: 7}, + }, slices.Collect(r6)) + + is.Equal([]lo.Tuple8[string, int, int, bool, float32, float64, int8, int16]{ + {A: "a", B: 1, C: 9, D: true, E: 0.1, F: 0.01, G: 1, H: 1}, + {A: "b", B: 2, C: 10, D: true, E: 0.2, F: 0.02, G: 2, H: 2}, + {A: "c", B: 3, C: 11, D: true, E: 0.3, F: 0.03, G: 3, H: 3}, + {A: "d", B: 4, C: 12, D: true, E: 0.4, F: 0.04, G: 4, H: 4}, + {A: "e", B: 5, C: 13, D: true, E: 0.5, F: 0.05, G: 5, H: 5}, + {A: "f", B: 6, C: 14, D: true, E: 0.6, F: 0.06, G: 6, H: 6}, + {A: "g", B: 7, C: 15, D: true, E: 0.7, F: 0.07, G: 7, H: 7}, + {A: "h", B: 8, C: 16, D: true, E: 0.8, F: 0.08, G: 8, H: 8}, + }, slices.Collect(r7)) + + is.Equal([]lo.Tuple9[string, int, int, bool, float32, float64, int8, int16, int32]{ + {A: "a", B: 1, C: 10, D: true, E: 0.1, F: 0.01, G: 1, H: 1, I: 1}, + {A: "b", B: 2, C: 11, D: true, E: 0.2, F: 0.02, G: 2, H: 2, I: 2}, + {A: "c", B: 3, C: 12, D: true, E: 0.3, F: 0.03, G: 3, H: 3, I: 3}, + {A: "d", B: 4, C: 13, D: true, E: 0.4, F: 0.04, G: 4, H: 4, I: 4}, + {A: "e", B: 5, C: 14, D: true, E: 0.5, F: 0.05, G: 5, H: 5, I: 5}, + {A: "f", B: 6, C: 15, D: true, E: 0.6, F: 0.06, G: 6, H: 6, I: 6}, + {A: "g", B: 7, C: 16, D: true, E: 0.7, F: 0.07, G: 7, H: 7, I: 7}, + {A: "h", B: 8, C: 17, D: true, E: 0.8, F: 0.08, G: 8, H: 8, I: 8}, + {A: "i", B: 9, C: 18, D: true, E: 0.9, F: 0.09, G: 9, H: 9, I: 9}, + }, slices.Collect(r8)) +} + +func TestCrossJoin(t *testing.T) { + t.Parallel() + is := assert.New(t) + + listOne := values("a", "b", "c") + listTwo := values(1, 2, 3) + emptyList := values[any]() + mixedList := values[any](9.6, 4, "foobar") + + results1 := CrossJoin2(emptyList, listTwo) + is.Empty(slices.Collect(results1)) + + results2 := CrossJoin2(listOne, emptyList) + is.Empty(slices.Collect(results2)) + + results3 := CrossJoin2(emptyList, emptyList) + is.Empty(slices.Collect(results3)) + + results4 := CrossJoin2(values("a"), listTwo) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("a", 2), lo.T2("a", 3)}, slices.Collect(results4)) + + results5 := CrossJoin2(listOne, values(1)) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("b", 1), lo.T2("c", 1)}, slices.Collect(results5)) + + results6 := CrossJoin2(listOne, listTwo) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("a", 2), lo.T2("a", 3), lo.T2("b", 1), lo.T2("b", 2), lo.T2("b", 3), lo.T2("c", 1), lo.T2("c", 2), lo.T2("c", 3)}, slices.Collect(results6)) + + results7 := CrossJoin2(listOne, mixedList) + is.Equal([]lo.Tuple2[string, any]{lo.T2[string, any]("a", 9.6), lo.T2[string, any]("a", 4), lo.T2[string, any]("a", "foobar"), lo.T2[string, any]("b", 9.6), lo.T2[string, any]("b", 4), lo.T2[string, any]("b", "foobar"), lo.T2[string, any]("c", 9.6), lo.T2[string, any]("c", 4), lo.T2[string, any]("c", "foobar")}, slices.Collect(results7)) +} + +func TestCrossJoinBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + listOne := values("a", "b", "c") + listTwo := values(1, 2, 3) + emptyList := values[any]() + mixedList := values[any](9.6, 4, "foobar") + + results1 := CrossJoinBy2(emptyList, listTwo, lo.T2[any, int]) + is.Empty(slices.Collect(results1)) + + results2 := CrossJoinBy2(listOne, emptyList, lo.T2[string, any]) + is.Empty(slices.Collect(results2)) + + results3 := CrossJoinBy2(emptyList, emptyList, lo.T2[any, any]) + is.Empty(slices.Collect(results3)) + + results4 := CrossJoinBy2(values("a"), listTwo, lo.T2[string, int]) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("a", 2), lo.T2("a", 3)}, slices.Collect(results4)) + + results5 := CrossJoinBy2(listOne, values(1), lo.T2[string, int]) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("b", 1), lo.T2("c", 1)}, slices.Collect(results5)) + + results6 := CrossJoinBy2(listOne, listTwo, lo.T2[string, int]) + is.Equal([]lo.Tuple2[string, int]{lo.T2("a", 1), lo.T2("a", 2), lo.T2("a", 3), lo.T2("b", 1), lo.T2("b", 2), lo.T2("b", 3), lo.T2("c", 1), lo.T2("c", 2), lo.T2("c", 3)}, slices.Collect(results6)) + + results7 := CrossJoinBy2(listOne, mixedList, lo.T2[string, any]) + is.Equal([]lo.Tuple2[string, any]{lo.T2[string, any]("a", 9.6), lo.T2[string, any]("a", 4), lo.T2[string, any]("a", "foobar"), lo.T2[string, any]("b", 9.6), lo.T2[string, any]("b", 4), lo.T2[string, any]("b", "foobar"), lo.T2[string, any]("c", 9.6), lo.T2[string, any]("c", 4), lo.T2[string, any]("c", "foobar")}, slices.Collect(results7)) +} diff --git a/it/type_manipulation.go b/it/type_manipulation.go new file mode 100644 index 0000000..a475166 --- /dev/null +++ b/it/type_manipulation.go @@ -0,0 +1,79 @@ +//go:build go1.23 + +package it + +import ( + "iter" + + "github.com/samber/lo" +) + +// ToSeqPtr returns a sequence of pointers to each value. +func ToSeqPtr[T any](collection iter.Seq[T]) iter.Seq[*T] { + return Map(collection, lo.ToPtr) +} + +// FromSeqPtr returns a sequence with the pointer values. +// Returns a zero value in case of a nil pointer element. +func FromSeqPtr[T any](collection iter.Seq[*T]) iter.Seq[T] { + return Map(collection, lo.FromPtr) +} + +// FromSeqPtrOr returns a sequence with the pointer values or the fallback value. +func FromSeqPtrOr[T any](collection iter.Seq[*T], fallback T) iter.Seq[T] { + return Map(collection, func(x *T) T { return lo.FromPtrOr(x, fallback) }) +} + +// ToAnySeq returns a sequence with all elements mapped to `any` type. +func ToAnySeq[T any](collection iter.Seq[T]) iter.Seq[any] { + return Map(collection, func(x T) any { return x }) +} + +// FromAnySeq returns a sequence with all elements mapped to a type. +// Panics on type conversion failure. +func FromAnySeq[T any](collection iter.Seq[any]) iter.Seq[T] { + return Map(collection, func(item any) T { + if t, ok := item.(T); ok { + return t + } + panic("it.FromAnySeq: type conversion failed") + }) +} + +// Empty returns an empty sequence. +func Empty[T any]() iter.Seq[T] { + return func(yield func(T) bool) {} +} + +// IsEmpty returns true if the sequence is empty. +// Will iterate at most once. +func IsEmpty[T any](collection iter.Seq[T]) bool { + for range collection { + return false + } + return true +} + +// IsNotEmpty returns true if the sequence is not empty. +// Will iterate at most once. +func IsNotEmpty[T any](collection iter.Seq[T]) bool { + return !IsEmpty(collection) +} + +// CoalesceSeq returns the first non-empty sequence. +// Will iterate through each sub-sequence at most once. +func CoalesceSeq[T any](v ...iter.Seq[T]) (iter.Seq[T], bool) { + for i := range v { + for range v[i] { + return v[i], true + } + } + return Empty[T](), false +} + +// CoalesceSeqOrEmpty returns the first non-empty sequence. +// Will iterate through each sub-sequence at most once. +func CoalesceSeqOrEmpty[T any](v ...iter.Seq[T]) iter.Seq[T] { + result, _ := CoalesceSeq(v...) + return result +} diff --git a/it/type_manipulation_test.go b/it/type_manipulation_test.go new file mode 100644 index 0000000..c54151f --- /dev/null +++ b/it/type_manipulation_test.go @@ -0,0 +1,167 @@ +//go:build go1.23 + +package it + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToSeqPtr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + str1 := "foo" + str2 := "bar" + result1 := ToSeqPtr(values(str1, str2)) + + is.Equal([]*string{&str1, &str2}, slices.Collect(result1)) +} + +func TestFromSeqPtr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + str1 := "foo" + str2 := "bar" + result1 := FromSeqPtr(values(&str1, &str2, nil)) + + is.Equal([]string{str1, str2, ""}, slices.Collect(result1)) +} + +func TestFromSeqPtrOr(t *testing.T) { + t.Parallel() + is := assert.New(t) + + str1 := "foo" + str2 := "bar" + result1 := FromSeqPtrOr(values(&str1, &str2, nil), "fallback") + + is.Equal([]string{str1, str2, "fallback"}, slices.Collect(result1)) +} + +func TestToAnySeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + in1 := values(0, 1, 2, 3) + in2 := values[int]() + out1 := ToAnySeq(in1) + out2 := ToAnySeq(in2) + + is.Equal([]any{0, 1, 2, 3}, slices.Collect(out1)) + is.Empty(slices.Collect(out2)) +} + +func TestFromAnySeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + out1 := FromAnySeq[string](values[any]("foobar", 42)) + out2 := FromAnySeq[string](values[any]("foobar", "42")) + + is.PanicsWithValue("it.FromAnySeq: type conversion failed", func() { _ = slices.Collect(out1) }) + is.Equal([]string{"foobar", "42"}, slices.Collect(out2)) +} + +func TestEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.Empty(slices.Collect(Empty[string]())) +} + +func TestIsEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.True(IsEmpty(values[string]())) + is.False(IsEmpty(values("foo"))) +} + +func TestIsNotEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + is.False(IsNotEmpty(values[string]())) + is.True(IsNotEmpty(values("foo"))) +} + +func TestCoalesceSeq(t *testing.T) { + t.Parallel() + is := assert.New(t) + + seq0 := values[int]() + seq1 := values(1) + seq2 := values(1, 2) + + result1, ok1 := CoalesceSeq[int]() + result4, ok4 := CoalesceSeq(seq0) + result6, ok6 := CoalesceSeq(seq2) + result7, ok7 := CoalesceSeq(seq1) + result8, ok8 := CoalesceSeq(seq1, seq2) + result9, ok9 := CoalesceSeq(seq2, seq1) + result10, ok10 := CoalesceSeq(seq0, seq1, seq2) + + is.NotNil(result1) + is.Empty(slices.Collect(result1)) + is.False(ok1) + + is.NotNil(result4) + is.Empty(slices.Collect(result4)) + is.False(ok4) + + is.NotNil(result6) + is.Equal(slices.Collect(seq2), slices.Collect(result6)) + is.True(ok6) + + is.NotNil(result7) + is.Equal(slices.Collect(seq1), slices.Collect(result7)) + is.True(ok7) + + is.NotNil(result8) + is.Equal(slices.Collect(seq1), slices.Collect(result8)) + is.True(ok8) + + is.NotNil(result9) + is.Equal(slices.Collect(seq2), slices.Collect(result9)) + is.True(ok9) + + is.NotNil(result10) + is.Equal(slices.Collect(seq1), slices.Collect(result10)) + is.True(ok10) +} + +func TestCoalesceSeqOrEmpty(t *testing.T) { + t.Parallel() + is := assert.New(t) + + seq0 := values[int]() + seq1 := values(1) + seq2 := values(1, 2) + + result1 := CoalesceSeqOrEmpty[int]() + result4 := CoalesceSeqOrEmpty(seq0) + result6 := CoalesceSeqOrEmpty(seq2) + result7 := CoalesceSeqOrEmpty(seq1) + result8 := CoalesceSeqOrEmpty(seq1, seq2) + result9 := CoalesceSeqOrEmpty(seq2, seq1) + result10 := CoalesceSeqOrEmpty(seq0, seq1, seq2) + + is.NotNil(result1) + is.Empty(slices.Collect(result1)) + is.NotNil(result4) + is.Empty(slices.Collect(result4)) + is.NotNil(result6) + is.Equal(slices.Collect(seq2), slices.Collect(result6)) + is.NotNil(result7) + is.Equal(slices.Collect(seq1), slices.Collect(result7)) + is.NotNil(result8) + is.Equal(slices.Collect(seq1), slices.Collect(result8)) + is.NotNil(result9) + is.Equal(slices.Collect(seq2), slices.Collect(result9)) + is.NotNil(result10) + is.Equal(slices.Collect(seq1), slices.Collect(result10)) +} diff --git a/slice_test.go b/slice_test.go index f248733..f3d819d 100644 --- a/slice_test.go +++ b/slice_test.go @@ -416,6 +416,7 @@ func TestShuffle(t *testing.T) { result2 := Shuffle([]int{}) is.NotEqual([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, result1) + is.ElementsMatch([]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, result1) is.Empty(result2) type myStrings []string diff --git a/string_test.go b/string_test.go index f8007c7..594fa88 100644 --- a/string_test.go +++ b/string_test.go @@ -2,9 +2,7 @@ package lo import ( "math" - "math/rand" "testing" - "time" "github.com/stretchr/testify/assert" ) @@ -13,8 +11,6 @@ func TestRandomString(t *testing.T) { t.Parallel() is := assert.New(t) - rand.Seed(time.Now().UnixNano()) - str1 := RandomString(100, LowerCaseLettersCharset) is.Equal(100, RuneLength(str1)) is.Subset(LowerCaseLettersCharset, []rune(str1))