Skip to content

Commit 8c82fb8

Browse files
authored
feat: add UnionBy and UnionByErr (#878)
Adds UnionBy and UnionByErr helpers to compute the union of multiple collections using a key iteratee, mirroring the pattern of UniqBy.
1 parent cd3e4f7 commit 8c82fb8

5 files changed

Lines changed: 246 additions & 0 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,8 @@ Supported intersection helpers:
265265
- [IntersectBy](#intersectby)
266266
- [Difference](#difference)
267267
- [Union](#union)
268+
- [UnionBy](#unionby)
269+
- [UnionByErr](#unionbyerr)
268270
- [Without](#without)
269271
- [WithoutBy](#withoutby)
270272
- [WithoutEmpty](#withoutempty)
@@ -3102,6 +3104,34 @@ union := lo.Union([]int{0, 1, 2, 3, 4, 5}, []int{0, 2}, []int{0, 10})
31023104
// []int{0, 1, 2, 3, 4, 5, 10}
31033105
```
31043106

3107+
### UnionBy
3108+
3109+
Returns all distinct elements from predicate returns. Result will not change the order of elements relatively.
3110+
3111+
```go
3112+
predicate := func(i int) int {
3113+
return i / 2
3114+
}
3115+
union := lo.UnionBy(predicate, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}, []int{0, 1, 11})
3116+
// []int{0, 2, 4, 10}
3117+
```
3118+
3119+
### UnionByErr
3120+
3121+
Returns all distinct elements from predicate returns. Result will not change the order of elements relatively. It returns the first error returned by the iteratee.
3122+
3123+
```go
3124+
predicate := func(i int) (int, error) {
3125+
if i == 42 {
3126+
return 0, errors.New("invalid value")
3127+
}
3128+
return i / 2, nil
3129+
}
3130+
union, err := lo.UnionByErr(predicate, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}, []int{0, 1, 11})
3131+
// union: []int{0, 2, 4, 10}
3132+
// err: nil
3133+
```
3134+
31053135
### Without
31063136

31073137
Returns a slice excluding all given values.

docs/data/core-unionby.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
name: UnionBy
3+
slug: unionby
4+
sourceRef: intersect.go#L260
5+
category: core
6+
subCategory: intersect
7+
playUrl:
8+
variantHelpers:
9+
- core#intersect#unionby
10+
similarHelpers:
11+
- core#intersect#union
12+
- core#intersect#intersect
13+
- core#intersect#intersectby
14+
- core#slice#uniq
15+
- core#slice#uniqby
16+
position: 110
17+
signatures:
18+
- "func UnionBy[T any, V comparable, Slice ~[]T](iteratee func(item T) V, lists ...Slice) Slice"
19+
---
20+
21+
Returns all distinct elements from multiple collections based on a key function. The result maintains the relative order of first occurrences.
22+
23+
```go
24+
lo.UnionBy(func(i int) int { return i / 2 }, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10})
25+
// []int{0, 2, 4, 10}
26+
```
27+
28+
```go
29+
lo.UnionBy(func(s string) string { return s[:1] }, []string{"foo", "bar"}, []string{"baz"})
30+
// []string{"foo", "baz"}
31+
```

docs/data/core-unionbyerr.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
name: UnionByErr
3+
slug: unionbyerr
4+
sourceRef: intersect.go#L285
5+
category: core
6+
subCategory: intersect
7+
playUrl:
8+
variantHelpers:
9+
- core#intersect#unionby
10+
- core#intersect#unionbyerr
11+
similarHelpers:
12+
- core#intersect#unionby
13+
- core#intersect#union
14+
- core#intersect#intersect
15+
- core#intersect#intersectby
16+
- core#slice#uniq
17+
- core#slice#uniqby
18+
position: 111
19+
signatures:
20+
- "func UnionByErr[T any, V comparable, Slice ~[]T](iteratee func(item T) (V, error), lists ...Slice) (Slice, error)"
21+
---
22+
23+
Returns all distinct elements from multiple collections based on a key function that can return an error. The result maintains the relative order of first occurrences. Returns the first error encountered from the iteratee.
24+
25+
```go
26+
lo.UnionByErr(func(i int) (int, error) { return i / 2, nil }, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10})
27+
// []int{0, 2, 4, 10}, nil
28+
```
29+
30+
```go
31+
lo.UnionByErr(func(s string) (string, error) { return s[:1], nil }, []string{"foo", "bar"}, []string{"baz"})
32+
// []string{"foo", "baz"}, nil
33+
```
34+
35+
```go
36+
lo.UnionByErr(func(i int) (int, error) {
37+
if i == 42 {
38+
return 0, errors.New("invalid value")
39+
}
40+
return i / 2, nil
41+
}, []int{0, 1, 2}, []int{42})
42+
// []int{0, 1}, error("invalid value")
43+
```

intersect.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,62 @@ func Union[T comparable, Slice ~[]T](lists ...Slice) Slice {
254254
return result
255255
}
256256

257+
// UnionBy is like Union except that it accepts iteratee which is invoked for each element of each collections
258+
// to generate the criterion by which uniqueness is computed.
259+
// Result values are chosen from the first collections in which the value occurs.
260+
// Play: TODO.
261+
func UnionBy[T any, V comparable, Slice ~[]T](iteratee func(item T) V, lists ...Slice) Slice {
262+
var capLen int
263+
264+
for _, list := range lists {
265+
capLen += len(list)
266+
}
267+
268+
result := make(Slice, 0, capLen)
269+
seen := make(map[V]struct{}, capLen)
270+
271+
for i := range lists {
272+
for j := range lists[i] {
273+
value := iteratee(lists[i][j])
274+
if _, ok := seen[value]; !ok {
275+
seen[value] = struct{}{}
276+
result = append(result, lists[i][j])
277+
}
278+
}
279+
}
280+
281+
return result
282+
}
283+
284+
// UnionByErr is like UnionBy except that it accepts iteratee which can return an error.
285+
// It returns the first error returned by the iteratee.
286+
// Play: TODO.
287+
func UnionByErr[T any, V comparable, Slice ~[]T](iteratee func(item T) (V, error), lists ...Slice) (Slice, error) {
288+
var capLen int
289+
290+
for _, list := range lists {
291+
capLen += len(list)
292+
}
293+
294+
result := make(Slice, 0, capLen)
295+
seen := make(map[V]struct{}, capLen)
296+
297+
for i := range lists {
298+
for j := range lists[i] {
299+
value, err := iteratee(lists[i][j])
300+
if err != nil {
301+
return nil, err
302+
}
303+
if _, ok := seen[value]; !ok {
304+
seen[value] = struct{}{}
305+
result = append(result, lists[i][j])
306+
}
307+
}
308+
}
309+
310+
return result, nil
311+
}
312+
257313
// Without returns a slice excluding all given values.
258314
// Play: https://go.dev/play/p/PcAVtYJsEsS
259315
func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice {

intersect_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,92 @@ func TestUnion(t *testing.T) {
308308
is.IsType(nonempty, allStrings, "type preserved")
309309
}
310310

311+
func TestUnionBy(t *testing.T) {
312+
t.Parallel()
313+
is := assert.New(t)
314+
315+
testFunc := func(i int) int {
316+
return i / 2
317+
}
318+
319+
result1 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10})
320+
result2 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{6, 7})
321+
result3 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{})
322+
result4 := UnionBy(testFunc, []int{0, 1, 2}, []int{0, 1, 2})
323+
result5 := UnionBy(testFunc, []int{}, []int{})
324+
is.Equal([]int{0, 2, 4, 10}, result1)
325+
is.Equal([]int{0, 2, 4, 6}, result2)
326+
is.Equal([]int{0, 2, 4}, result3)
327+
is.Equal([]int{0, 2}, result4)
328+
is.Equal([]int{}, result5)
329+
330+
result11 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}, []int{0, 1, 11})
331+
result12 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{6, 7}, []int{8, 9})
332+
result13 := UnionBy(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{}, []int{})
333+
result14 := UnionBy(testFunc, []int{0, 1, 2}, []int{0, 1, 2}, []int{0, 1, 2})
334+
result15 := UnionBy(testFunc, []int{}, []int{}, []int{})
335+
is.Equal([]int{0, 2, 4, 10}, result11)
336+
is.Equal([]int{0, 2, 4, 6, 8}, result12)
337+
is.Equal([]int{0, 2, 4}, result13)
338+
is.Equal([]int{0, 2}, result14)
339+
is.Equal([]int{}, result15)
340+
}
341+
342+
func TestUnionByErr(t *testing.T) {
343+
t.Parallel()
344+
is := assert.New(t)
345+
346+
testFunc := func(i int) (int, error) {
347+
return i / 2, nil
348+
}
349+
350+
result1, err1 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10})
351+
result2, err2 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{6, 7})
352+
result3, err3 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{})
353+
result4, err4 := UnionByErr(testFunc, []int{0, 1, 2}, []int{0, 1, 2})
354+
result5, err5 := UnionByErr(testFunc, []int{}, []int{})
355+
is.NoError(err1)
356+
is.NoError(err2)
357+
is.NoError(err3)
358+
is.NoError(err4)
359+
is.NoError(err5)
360+
is.Equal([]int{0, 2, 4, 10}, result1)
361+
is.Equal([]int{0, 2, 4, 6}, result2)
362+
is.Equal([]int{0, 2, 4}, result3)
363+
is.Equal([]int{0, 2}, result4)
364+
is.Equal([]int{}, result5)
365+
366+
result11, err11 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10}, []int{0, 1, 11})
367+
result12, err12 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{6, 7}, []int{8, 9})
368+
result13, err13 := UnionByErr(testFunc, []int{0, 1, 2, 3, 4, 5}, []int{}, []int{})
369+
result14, err14 := UnionByErr(testFunc, []int{0, 1, 2}, []int{0, 1, 2}, []int{0, 1, 2})
370+
result15, err15 := UnionByErr(testFunc, []int{}, []int{}, []int{})
371+
is.NoError(err11)
372+
is.NoError(err12)
373+
is.NoError(err13)
374+
is.NoError(err14)
375+
is.NoError(err15)
376+
is.Equal([]int{0, 2, 4, 10}, result11)
377+
is.Equal([]int{0, 2, 4, 6, 8}, result12)
378+
is.Equal([]int{0, 2, 4}, result13)
379+
is.Equal([]int{0, 2}, result14)
380+
is.Equal([]int{}, result15)
381+
382+
// Test error case
383+
errFunc := func(i int) (int, error) {
384+
if i == 2 {
385+
return 0, assert.AnError
386+
}
387+
return i / 2, nil
388+
}
389+
390+
_, err6 := UnionByErr(errFunc, []int{0, 1, 2, 3, 4, 5}, []int{0, 2, 10})
391+
is.Error(err6)
392+
393+
_, err7 := UnionByErr(errFunc, []int{0, 1, 3, 4, 5}, []int{2, 10})
394+
is.Error(err7)
395+
}
396+
311397
func TestWithout(t *testing.T) {
312398
t.Parallel()
313399
is := assert.New(t)

0 commit comments

Comments
 (0)