Refactoring lo.IntersectBy + adding loit.IntersectBy + adding doc (#739)

* feat(intersectby): form transform callback in first position and add support for vaarg

* feat(it): adding loit.IntersectBy

* doc: adding lo.IntersectBy + loit.IntersectBy

* doc: adding lo.IntersectBy + loit.IntersectBy

* style: fix linter

* doc: adding example for lo.IntersectBy
This commit is contained in:
Samuel Berthe
2025-11-07 01:23:34 +01:00
committed by GitHub
parent 43ae3d74da
commit d99edab80d
12 changed files with 286 additions and 30 deletions

View File

@@ -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.

View File

@@ -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}
```

View File

@@ -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}
```

View File

@@ -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
---

View File

@@ -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)
```

View File

@@ -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).

View File

@@ -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

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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)

View File

@@ -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]
}