diff --git a/README.md b/README.md index c80b363..74bf5b8 100644 --- a/README.md +++ b/README.md @@ -246,6 +246,7 @@ Supported intersection helpers: - [None](#none) - [NoneBy](#noneby) - [Intersect](#intersect) +- [IntersectBy](#intersectby) - [Difference](#difference) - [Union](#union) - [Without](#without) @@ -2597,11 +2598,32 @@ result2 := lo.Intersect([]int{0, 1, 2, 3, 4, 5}, []int{0, 6}) result3 := lo.Intersect([]int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) // []int{} - result4 := lo.Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) // []int{3} ``` +### IntersectBy + +Returns the intersection between two collections using a custom key selector function. + +```go +transform := func(v int) string { + return strconv.Itoa(v) +} + +result1 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{0, 2}) +// []int{0, 2} + +result2 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{0, 6}) +// []int{0} + +result3 := lo.IntersectBy(transform, []int{0, 1, 2, 3, 4, 5}, []int{-1, 6}) +// []int{} + +result4 := lo.IntersectBy(transform, []int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) +// []int{3} +``` + ### Difference Returns the difference between two collections. diff --git a/docs/data/core-intersect.md b/docs/data/core-intersect.md index 7b26027..f00d75d 100644 --- a/docs/data/core-intersect.md +++ b/docs/data/core-intersect.md @@ -8,6 +8,9 @@ playUrl: https://go.dev/play/p/uuElL9X9e58 variantHelpers: - core#intersect#intersect similarHelpers: + - core#intersect#intersectby + - it#intersect#intersect + - it#intersect#intersectby - core#intersect#difference - core#intersect#union - core#intersect#without @@ -20,8 +23,6 @@ signatures: Returns the intersection between collections. ```go -lo.Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0})) +lo.Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) // []int{3} ``` - - diff --git a/docs/data/core-intersectby.md b/docs/data/core-intersectby.md new file mode 100644 index 0000000..7435ba1 --- /dev/null +++ b/docs/data/core-intersectby.md @@ -0,0 +1,32 @@ +--- +name: IntersectBy +slug: intersectby +sourceRef: intersect.go#L174 +category: core +subCategory: intersect +playUrl: +variantHelpers: + - core#intersect#intersectby +similarHelpers: + - core#intersect#intersect + - it#intersect#intersect + - it#intersect#intersectby + - core#intersect#difference + - core#intersect#union + - core#intersect#without + - core#slice#uniq +position: 80 +signatures: + - "func IntersectBy[T any, K comparable, Slice ~[]T](transform func(T) K, lists ...Slice) Slice" +--- + +Returns the intersection between two collections using a custom key selector function. + +```go +transform := func(v int) string { + return strconv.Itoa(v) +} + +lo.IntersectBy(transform, []int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) +// []int{3} +``` diff --git a/docs/data/it-intersect.md b/docs/data/it-intersect.md index 9ebb638..9da67b2 100644 --- a/docs/data/it-intersect.md +++ b/docs/data/it-intersect.md @@ -10,7 +10,9 @@ playUrl: "https://go.dev/play/p/kz3cGhGZZWF" variantHelpers: - it#intersect#intersect similarHelpers: + - it#intersect#intersectby - core#slice#intersect + - core#slice#intersectby - it#intersect#union position: 10 --- diff --git a/docs/data/it-intersectby.md b/docs/data/it-intersectby.md new file mode 100644 index 0000000..3ca0b03 --- /dev/null +++ b/docs/data/it-intersectby.md @@ -0,0 +1,53 @@ +--- +name: IntersectBy +slug: intersectby +sourceRef: it/intersect.go#L78 +category: it +subCategory: intersect +signatures: + - "func IntersectBy[T any, K comparable, I ~func(func(T) bool)](func(T) K, lists ...I) I" +playUrl: +variantHelpers: + - it#intersect#intersectby +similarHelpers: + - it#intersect#intersect + - core#slice#intersect + - core#slice#intersectby + - it#intersect#union +position: 10 +--- + +Returns the intersection between given collections using a custom key selector function. + +Examples: + +```go +seq1 := func(yield func(int) bool) { + _ = yield(1) + _ = yield(2) + _ = yield(3) + _ = yield(4) +} +seq2 := func(yield func(int) bool) { + _ = yield(2) + _ = yield(3) + _ = yield(5) +} +seq3 := func(yield func(int) bool) { + _ = yield(3) + _ = yield(2) + _ = yield(6) +} + +transform := func(v int) string { + return strconv.Itoa(v) +} + +intersection := it.IntersectBy(transform, seq1, seq2, seq3) + +var result []int +for v := range intersection { + result = append(result, v) +} +// result contains 2, 3 (elements present in all sequences) +``` \ No newline at end of file diff --git a/docs/docs/contributing.md b/docs/docs/contributing.md index 48eb545..b863a16 100644 --- a/docs/docs/contributing.md +++ b/docs/docs/contributing.md @@ -51,3 +51,7 @@ Please add an example of your helper in the file named `xxxx_example_test.go`. I 1- If a callback returns a single bool then it should probably be called "predicate". 2- If a callback is used to change a collection element into something else then it should probably be called "transform". 3- If a callback returns nothing (void) then it should probably be called "callback". + +### Types + +1- Generic functions must preserve the underlying type of collections so that the returned values maintain the same type as the input. See [#365](https://github.com/samber/lo/pull/365/files). diff --git a/docs/static/llms.txt b/docs/static/llms.txt index c78e149..3822dd5 100644 --- a/docs/static/llms.txt +++ b/docs/static/llms.txt @@ -137,6 +137,7 @@ Lo is built on a foundation of pragmatic engineering principles that balance pow - None: Check if no elements in collection match subset - NoneBy: Check if no elements match predicate - Intersect: Get elements common to all collections +- IntersectBy: Get elements common to all collections with key selector - Difference: Get elements in first collection but not in others - Union: Get all unique elements from collections - Without: Get collection with specified elements removed @@ -404,6 +405,7 @@ The lo/it package provides iterator helpers for lazy evaluation and streaming op - None: Check if no elements in sequence match subset - NoneBy: Check if no elements match predicate - Intersect: Get elements common to all sequences +- IntersectBy: Get elements common to all sequences with key selector - Union: Get all unique elements from sequences - Without: Get sequence with specified elements excluded - WithoutBy: Get sequence with elements excluded by key transform diff --git a/intersect.go b/intersect.go index a49b9fc..5a45350 100644 --- a/intersect.go +++ b/intersect.go @@ -171,24 +171,61 @@ func Intersect[T comparable, Slice ~[]T](lists ...Slice) Slice { } // IntersectBy returns the intersection between two collections using a custom key selector function. -// It preserves the order of elements from the second list (list2). -func IntersectBy[T any, K comparable, Slice ~[]T](list1 Slice, list2 Slice, iteratee func(T) K) Slice { - result := make(Slice, 0) - seen := make(map[K]struct{}) - - for _, item := range list1 { - key := iteratee(item) - seen[key] = struct{}{} +func IntersectBy[T any, K comparable, Slice ~[]T](transform func(T) K, lists ...Slice) Slice { + if len(lists) == 0 { + return Slice{} } - for _, item := range list2 { - key := iteratee(item) - if _, exists := seen[key]; exists { - result = append(result, item) + if len(lists) == 1 { + return lists[0] + } + + seen := make(map[K]bool) + + for i := len(lists) - 1; i >= 0; i-- { + if i == len(lists)-1 { + for _, item := range lists[i] { + k := transform(item) + seen[k] = true + } + continue + } + + if i == 0 { + result := make(Slice, 0, len(seen)) + for _, item := range lists[0] { + k := transform(item) + if _, ok := seen[k]; ok { + result = append(result, item) + delete(seen, k) + } + } + return result + } + + for k := range seen { + seen[k] = false + } + + for _, item := range lists[i] { + k := transform(item) + if _, ok := seen[k]; ok { + seen[k] = true + } + } + + for k, v := range seen { + if !v { + delete(seen, k) + } + } + + if len(seen) == 0 { + break } } - return result + return Slice{} } // Difference returns the difference between two collections. diff --git a/intersect_test.go b/intersect_test.go index cae7e8c..e22cfed 100644 --- a/intersect_test.go +++ b/intersect_test.go @@ -226,23 +226,23 @@ func TestIntersectBy(t *testing.T) { {ID: 4, Name: "Alice"}, } - intersectByID := IntersectBy(list1, list2, func(u User) int { + intersectByID := IntersectBy(func(u User) int { return u.ID - }) - is.Equal(intersectByID, []User{{ID: 2, Name: "Robert"}, {ID: 3, Name: "Charlie"}}) - // output: [{2 Robert} {3 Charlie}] + }, list1, list2) + is.ElementsMatch(intersectByID, []User{{ID: 2, Name: "Bob"}, {ID: 3, Name: "Charlie"}}) - intersectByName := IntersectBy(list1, list2, func(u User) string { + intersectByName := IntersectBy(func(u User) string { return u.Name - }) - is.Equal(intersectByName, []User{{ID: 3, Name: "Charlie"}, {ID: 4, Name: "Alice"}}) - // output: [{3 Charlie} {4 Alice}] + }, list1, list2) + is.ElementsMatch(intersectByName, []User{{ID: 3, Name: "Charlie"}, {ID: 1, Name: "Alice"}}) - intersectByIDAndName := IntersectBy(list1, list2, func(u User) string { + intersectByIDAndName := IntersectBy(func(u User) string { return strconv.Itoa(u.ID) + u.Name - }) - is.Equal(intersectByIDAndName, []User{{ID: 3, Name: "Charlie"}}) - // output: [{3 Charlie}] + }, list1, list2) + is.ElementsMatch(intersectByIDAndName, []User{{ID: 3, Name: "Charlie"}}) + + result := IntersectBy(strconv.Itoa, []int{0, 6, 0, 3}, []int{0, 1, 2, 3, 4, 5}, []int{0, 6}) + is.ElementsMatch(result, []int{0}) } func TestDifference(t *testing.T) { diff --git a/it/intersect.go b/it/intersect.go index 27a6090..0aee8cc 100644 --- a/it/intersect.go +++ b/it/intersect.go @@ -151,6 +151,68 @@ func Intersect[T comparable, I ~func(func(T) bool)](lists ...I) I { //nolint:goc } } +// IntersectBy returns the intersection between given collections using a +// custom key selector function. +// Will allocate a map large enough to hold all distinct elements. +// Long heterogeneous input sequences can cause excessive memory usage. +func IntersectBy[T any, K comparable, I ~func(func(T) bool)](transform func(T) K, 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[K]bool) + + for i := len(lists) - 1; i >= 0; i-- { + if i == len(lists)-1 { + for item := range lists[i] { + k := transform(item) + seen[k] = true + } + continue + } + + if i == 0 { + for item := range lists[0] { + k := transform(item) + if _, ok := seen[k]; ok { + if !yield(item) { + return + } + delete(seen, k) + } + } + continue + } + + for k := range seen { + seen[k] = false + } + + for item := range lists[i] { + k := transform(item) + if _, ok := seen[k]; ok { + seen[k] = 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. diff --git a/it/intersect_test.go b/it/intersect_test.go index 4ab3fdf..996c0a6 100644 --- a/it/intersect_test.go +++ b/it/intersect_test.go @@ -5,6 +5,7 @@ package it import ( "iter" "slices" + "strconv" "testing" "github.com/stretchr/testify/assert" @@ -206,6 +207,38 @@ func TestIntersect(t *testing.T) { is.IsType(nonempty, allStrings, "type preserved") } +func TestIntersectBy(t *testing.T) { + t.Parallel() + is := assert.New(t) + + transform := strconv.Itoa + + result1 := IntersectBy(transform, []iter.Seq[int]{}...) + result2 := IntersectBy(transform, values(0, 1, 2, 3, 4, 5)) + result3 := IntersectBy(transform, values(0, 1, 2, 3, 4, 5), values(0, 6)) + result4 := IntersectBy(transform, values(0, 1, 2, 3, 4, 5), values(-1, 6)) + result5 := IntersectBy(transform, values(0, 6, 0), values(0, 1, 2, 3, 4, 5)) + result6 := IntersectBy(transform, values(0, 1, 2, 3, 4, 5), values(0, 6, 0)) + result7 := IntersectBy(transform, values(0, 1, 2), values(1, 2, 3), values(2, 3, 4)) + result8 := IntersectBy(transform, values(0, 1, 2), values(1, 2, 3), values(2, 3, 4), values(3, 4, 5)) + result9 := IntersectBy(transform, 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 := IntersectBy(func(s string) string { return s + s }, allStrings, allStrings) + is.IsType(nonempty, allStrings, "type preserved") +} + func TestUnion(t *testing.T) { t.Parallel() is := assert.New(t) diff --git a/lo_example_test.go b/lo_example_test.go index 4eb30b5..3e1b3b4 100644 --- a/lo_example_test.go +++ b/lo_example_test.go @@ -3343,7 +3343,15 @@ func ExampleCrossJoinBy9() { } func ExampleIntersect() { - fmt.Printf("%v", Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0})) + result := Intersect([]int{0, 3, 5, 7}, []int{3, 5}, []int{0, 1, 2, 0, 3, 0}) + fmt.Printf("%v", result) // Output: // [3] } + +func ExampleIntersectBy() { + result := IntersectBy(strconv.Itoa, []int{0, 6, 0, 3}, []int{0, 1, 2, 3, 4, 5}, []int{0, 6}) + fmt.Printf("%v", result) + // Output: + // [0] +}