feat(sieve): add the remaining collection helpers and docs mentions

- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition
  and Find, plus the Set type built with SetOf (Has, Add, Slice).
- Add unit tests at 100% statement coverage and a runnable example for
  every function and Set method.
- Complete the module README: contracts, use cases, Pluck rationale,
  standard-library pointers and the full API reference.
- docs/architecture/introduction.md: add a Utilities row for sieve.
- docs/setup/coming-from-wintercms.md: map Laravel collection methods
  to sieve and narrow the not-provided row to the fluent collection
  object.
This commit is contained in:
2026-10-11 14:27:13 +02:00
parent 6933004b87
commit a0bf8ece05
7 changed files with 677 additions and 12 deletions

View File

@@ -8,38 +8,123 @@ Generic, dependency-free collection helpers that the standard library slices and
## Overview
`sieve` gives plain Go slices the collection operations that WinterCMS code reaches for on Laravel collections. Every function is eager and never modifies its input, results never share a backing array with the input, callbacks run once per element in input order, and nil or empty input yields a non-nil empty result, so `encoding/json` writes `[]` rather than `null`.
`sieve` gives plain Go slices the collection operations that WinterCMS code reaches for on Laravel collections, the objects that Eloquent queries return (`map`, `filter`, `reduce`, `keyBy`, `groupBy`, `unique`, `partition`, `first`). It is a set of generic functions rather than a fluent collection object, so type inference works from the slice argument and no wrapper type leaks into your models.
Four contracts hold for every function:
- It is eager and never modifies its input.
- Its result never shares a backing array with the input, so changing or appending to a result leaves the caller's slice unchanged.
- Callbacks run once per element, in input order. `sieve.TryMap` and `sieve.Find` stop early.
- Nil or empty input yields a non-nil empty slice or map, so `encoding/json` writes `[]` or `{}` rather than `null`.
## Role in the framework
A dependency-free leaf with no imports, so any module, plugin or application can use it without import cycles.
A dependency-free leaf with no imports, so any module, plugin or application can use it without import cycles. It complements the standard `slices` and `maps` packages rather than wrapping them: nothing here duplicates a function those packages already provide.
Use cases:
- Shape API responses from models, for example turning a slice of records into a slice of response structs.
- Key records by ID to join two result sets without a nested loop.
- Group records by a field, such as posts by author.
- Deduplicate IDs before an `IN` query.
- Check set membership, such as whether an ID is in an allow list.
## Features
- `sieve.Map` applies a function to every element and returns the results in order; with a field accessor it is the pluck operation.
- `sieve.Map` applies a function to every element; with a field accessor it is the pluck operation.
- `sieve.TryMap` maps with a function that can fail and stops at the first error, returning it unchanged.
- `sieve.Filter` keeps the elements that match a predicate, and `sieve.Partition` splits a slice into the matches and the rest.
- `sieve.Reduce` folds a slice from left to right into an accumulator of any type.
- `sieve.KeyBy` builds a map from a key to an element (later items win), and `sieve.GroupBy` builds a map from a key to the elements with that key, in input order.
- `sieve.Uniq` removes duplicates of comparable values, and `sieve.UniqBy` removes elements whose key was already seen; both keep the first occurrence.
- `sieve.Find` returns the first element that matches a predicate, with a found flag.
- `sieve.Set`, built with `sieve.SetOf`, is a plain map-backed set with `sieve.Set.Has`, `sieve.Set.Add` and `sieve.Set.Slice`.
- Functions that keep the element type (`sieve.Filter`, `sieve.Uniq`, `sieve.UniqBy`, `sieve.Partition` and the groups of `sieve.GroupBy`) return the caller's named slice type.
## Usage
```go
package blog
import "git.golem15.com/golem15/summercms/modules/sieve"
import (
"fmt"
"strconv"
"git.golem15.com/golem15/summercms/modules/sieve"
)
type Post struct {
ID int
Title string
ID int
Author string
Title string
Published bool
}
func titles(posts []Post) []string {
return sieve.Map(posts, func(p Post) string { return p.Title })
type PostResponse struct {
ID int `json:"id"`
Title string `json:"title"`
}
func shape(posts []Post, rawIDs []string) error {
// Keep the published posts and turn them into response structs.
published := sieve.Filter(posts, func(p Post) bool { return p.Published })
body := sieve.Map(published, func(p Post) PostResponse {
return PostResponse{ID: p.ID, Title: p.Title}
})
// Key by ID for lookups, group by author, keep one post per author.
byID := sieve.KeyBy(posts, func(p Post) int { return p.ID })
byAuthor := sieve.GroupBy(posts, func(p Post) string { return p.Author })
firstPerAuthor := sieve.UniqBy(posts, func(p Post) string { return p.Author })
// Split drafts from published posts, and find the first draft.
live, drafts := sieve.Partition(posts, func(p Post) bool { return p.Published })
firstDraft, found := sieve.Find(posts, func(p Post) bool { return !p.Published })
// Parse IDs from a query string, stopping at the first bad one.
ids, err := sieve.TryMap(rawIDs, strconv.Atoi)
if err != nil {
return fmt.Errorf("blog: bad post id: %w", err)
}
wanted := sieve.SetOf(ids...)
fmt.Println(len(body), len(byID), len(byAuthor), len(firstPerAuthor),
len(live), len(drafts), firstDraft.Title, found, wanted.Has(1))
return nil
}
```
There is no Pluck. Laravel's `pluck('title')` takes a field name as a string, which in Go would need reflection and would give up compile-time type checking. The typed equivalent is `sieve.Map(posts, func(p Post) string { return p.Title })`.
What the standard library already covers:
- `slices.Chunk` splits a slice into chunks lazily; use `slices.Collect(slices.Chunk(s, n))` for an eager `[][]T`.
- `slices.Contains` and `slices.IndexFunc` test for and locate elements.
- `slices.Sort` and `slices.SortFunc` sort in place.
- `slices.Min` and `slices.Max` return the smallest and largest element.
- `slices.Compact` removes consecutive duplicates only, in place.
- `slices.DeleteFunc` filters in place.
- `maps.Keys` and `maps.Values` iterate a map; wrap them in `slices.Sorted` for ordered output.
## API reference
| Identifier | Description |
|------------|-------------|
| `sieve.Map` | Returns f applied to each element, in input order. Nil input gives a non-nil empty slice. |
| `sieve.TryMap` | Like Map with a fallible f. Stops at the first error and returns nil with that error unchanged. Nil input gives a non-nil empty slice and a nil error. |
| `sieve.Filter` | Returns the elements for which keep is true, in input order, as the caller's slice type. Nil input or no matches gives a non-nil empty slice. |
| `sieve.Reduce` | Folds the elements from left to right, starting from init. The accumulator type may differ from the element type. Nil input returns init. |
| `sieve.KeyBy` | Returns a map from key to element; when two elements share a key, the later one wins. Nil input gives a non-nil empty map. |
| `sieve.GroupBy` | Returns a map from key to the elements with that key, each group in input order and of the caller's slice type. Nil input gives a non-nil empty map. |
| `sieve.Uniq` | Returns the elements without duplicates, keeping the first occurrence, in input order. Nil input gives a non-nil empty slice. |
| `sieve.UniqBy` | Returns the elements whose key was not seen before, keeping the first occurrence of each key, in input order. Nil input gives a non-nil empty slice. |
| `sieve.Partition` | Returns (matched, rest), both in input order and both non-nil, even for nil input. |
| `sieve.Find` | Returns the first matching element and true; the predicate is not called after the first match. No match or nil input gives the zero value and false. |
| `sieve.Set` | A set backed by `map[T]struct{}`; `len`, `range`, `delete` and `maps.Keys` work on it directly. Has and Slice work on a nil Set; Add on a nil Set panics. |
| `sieve.SetOf` | Returns a new set holding the given items without duplicates. With no arguments it returns an empty, non-nil set that Add can use. |
| `sieve.Set.Has` | Reports whether a value is in the set. A nil Set reports false. |
| `sieve.Set.Add` | Adds one or more values; values already present are ignored. Panics on a nil Set. |
| `sieve.Set.Slice` | Returns each element exactly once, in unspecified order. Non-nil even for an empty or nil Set. |
## Dependencies
@@ -53,4 +138,4 @@ func titles(posts []Post) []string {
go test ./modules/sieve/...
```
The tests are in-memory and need no external services.
The tests are in-memory and need no external services. They include runnable examples for every function and `sieve.Set` method, and cover every statement.

View File

@@ -2,13 +2,22 @@ package sieve_test
import (
"fmt"
"slices"
"strconv"
"git.golem15.com/golem15/summercms/modules/sieve"
)
type post struct {
ID int
Title string
ID int
Author string
Title string
}
var posts = []post{
{ID: 1, Author: "ana", Title: "Hello"},
{ID: 2, Author: "bob", Title: "Summer"},
{ID: 3, Author: "ana", Title: "Goodbye"},
}
func ExampleMap() {
@@ -22,3 +31,96 @@ func ExampleMap() {
fmt.Println(titles)
// Output: [Hello Summer Goodbye]
}
func ExampleTryMap() {
nums, err := sieve.TryMap([]string{"1", "2", "3"}, strconv.Atoi)
fmt.Println(nums, err)
nums, err = sieve.TryMap([]string{"1", "x", "3"}, strconv.Atoi)
fmt.Println(nums, err)
// Output:
// [1 2 3] <nil>
// [] strconv.Atoi: parsing "x": invalid syntax
}
func ExampleFilter() {
byAna := sieve.Filter(posts, func(p post) bool { return p.Author == "ana" })
fmt.Println(len(byAna), byAna[0].Title, byAna[1].Title)
// Output: 2 Hello Goodbye
}
func ExampleReduce() {
total := sieve.Reduce([]int{1, 2, 3, 4}, 0, func(sum, v int) int { return sum + v })
fmt.Println(total)
// Output: 10
}
func ExampleKeyBy() {
byID := sieve.KeyBy(posts, func(p post) int { return p.ID })
fmt.Println(byID[2].Title, len(byID))
// Output: Summer 3
}
func ExampleGroupBy() {
byAuthor := sieve.GroupBy(posts, func(p post) string { return p.Author })
titles := make(map[string][]string, len(byAuthor))
for author, group := range byAuthor {
titles[author] = sieve.Map(group, func(p post) string { return p.Title })
}
fmt.Println(titles)
// Output: map[ana:[Hello Goodbye] bob:[Summer]]
}
func ExampleUniq() {
fmt.Println(sieve.Uniq([]int{3, 1, 3, 2, 1}))
// Output: [3 1 2]
}
func ExampleUniqBy() {
firstPerAuthor := sieve.UniqBy(posts, func(p post) string { return p.Author })
fmt.Println(sieve.Map(firstPerAuthor, func(p post) string { return p.Title }))
// Output: [Hello Summer]
}
func ExamplePartition() {
even, odd := sieve.Partition([]int{1, 2, 3, 4, 5}, func(v int) bool { return v%2 == 0 })
fmt.Println(even, odd)
// Output: [2 4] [1 3 5]
}
func ExampleFind() {
p, ok := sieve.Find(posts, func(p post) bool { return p.Author == "bob" })
fmt.Println(p.Title, ok)
_, ok = sieve.Find(posts, func(p post) bool { return p.Author == "acme" })
fmt.Println(ok)
// Output:
// Summer true
// false
}
func ExampleSetOf() {
tags := sieve.SetOf("blog", "news", "blog")
fmt.Println(len(tags))
// Output: 2
}
func ExampleSet_Has() {
tags := sieve.SetOf("blog", "news")
fmt.Println(tags.Has("blog"), tags.Has("acme"))
// Output: true false
}
func ExampleSet_Add() {
tags := sieve.SetOf[string]()
tags.Add("blog", "news", "blog")
fmt.Println(len(tags), tags.Has("news"))
// Output: 2 true
}
func ExampleSet_Slice() {
ids := sieve.SetOf(3, 1, 2, 3).Slice()
slices.Sort(ids)
fmt.Println(ids)
// Output: [1 2 3]
}

43
modules/sieve/set.go Normal file
View File

@@ -0,0 +1,43 @@
package sieve
// Set is a set of comparable values backed by a plain map, so len, range,
// delete and maps.Keys work on it directly. Create sets with SetOf: Has and
// Slice work on a nil Set, but Add on a nil Set panics like any write to a nil
// map. Slice returns the elements in unspecified order; use
// slices.Sorted(maps.Keys(set)) for ordered output when T is ordered.
type Set[T comparable] map[T]struct{}
// SetOf returns a new set holding items, without duplicates. SetOf() with no
// arguments returns an empty, non-nil set that Add can use. It accepts a slice
// spread with ..., including a named slice type.
func SetOf[T comparable](items ...T) Set[T] {
s := make(Set[T], len(items))
for _, v := range items {
s[v] = struct{}{}
}
return s
}
// Has reports whether v is in the set. A nil Set has no elements.
func (s Set[T]) Has(v T) bool {
_, ok := s[v]
return ok
}
// Add puts items into the set; items already present are ignored. Add on a nil
// Set panics, so create sets with SetOf.
func (s Set[T]) Add(items ...T) {
for _, v := range items {
s[v] = struct{}{}
}
}
// Slice returns a new slice holding each element of the set exactly once, in
// unspecified order. It is non-nil even for an empty or nil Set.
func (s Set[T]) Slice() []T {
out := make([]T, 0, len(s))
for v := range s {
out = append(out, v)
}
return out
}

View File

@@ -20,3 +20,133 @@ func Map[S ~[]T, T, U any](s S, f func(T) U) []U {
}
return out
}
// TryMap returns a new slice holding f applied to each element of s, in order.
// It stops at the first error and returns nil together with that error,
// unchanged; f is not called for the remaining elements. A caller that needs
// the failing index wraps the error inside f. A nil or empty s yields a
// non-nil empty slice and a nil error.
func TryMap[S ~[]T, T, U any](s S, f func(T) (U, error)) ([]U, error) {
out := make([]U, 0, len(s))
for _, v := range s {
u, err := f(v)
if err != nil {
return nil, err
}
out = append(out, u)
}
return out, nil
}
// Filter returns a new slice holding the elements of s for which keep returns
// true, in input order. The result has the caller's slice type. A nil or empty
// s, or one with no matches, yields a non-nil empty slice.
func Filter[S ~[]T, T any](s S, keep func(T) bool) S {
out := make(S, 0)
for _, v := range s {
if keep(v) {
out = append(out, v)
}
}
return out
}
// Reduce folds s from left to right: it starts with init and replaces the
// accumulator with f(accumulator, element) for each element in order. The
// accumulator type may differ from the element type. A nil or empty s returns
// init unchanged.
func Reduce[S ~[]T, T, A any](s S, init A, f func(A, T) A) A {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// KeyBy returns a new map from key(element) to element. When two elements
// share a key, the later one wins. key is called once per element, in order.
// A nil or empty s yields a non-nil empty map.
func KeyBy[S ~[]T, T any, K comparable](s S, key func(T) K) map[K]T {
out := make(map[K]T, len(s))
for _, v := range s {
out[key(v)] = v
}
return out
}
// GroupBy returns a new map from key(element) to the elements with that key.
// Each group keeps input order and has the caller's slice type; groups are
// independent of the input and of each other. key is called once per element,
// in order. A nil or empty s yields a non-nil empty map.
func GroupBy[S ~[]T, T any, K comparable](s S, key func(T) K) map[K]S {
out := make(map[K]S)
for _, v := range s {
k := key(v)
out[k] = append(out[k], v)
}
return out
}
// Uniq returns a new slice holding the elements of s without duplicates,
// keeping the first occurrence of each, in input order. The result has the
// caller's slice type. A nil or empty s yields a non-nil empty slice.
func Uniq[S ~[]T, T comparable](s S) S {
out := make(S, 0, len(s))
seen := make(map[T]struct{}, len(s))
for _, v := range s {
if _, ok := seen[v]; ok {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
return out
}
// UniqBy returns a new slice holding the elements of s whose key(element) has
// not been seen before, keeping the first occurrence of each key, in input
// order. key is called once per element. The result has the caller's slice
// type. A nil or empty s yields a non-nil empty slice.
func UniqBy[S ~[]T, T any, K comparable](s S, key func(T) K) S {
out := make(S, 0, len(s))
seen := make(map[K]struct{}, len(s))
for _, v := range s {
k := key(v)
if _, ok := seen[k]; ok {
continue
}
seen[k] = struct{}{}
out = append(out, v)
}
return out
}
// Partition splits s into two new slices: matched holds the elements for which
// match returns true and rest holds the others, both in input order and both
// with the caller's slice type. match is called once per element. Both results
// are non-nil, even when empty, and for a nil or empty s.
func Partition[S ~[]T, T any](s S, match func(T) bool) (matched, rest S) {
matched = make(S, 0)
rest = make(S, 0)
for _, v := range s {
if match(v) {
matched = append(matched, v)
} else {
rest = append(rest, v)
}
}
return matched, rest
}
// Find returns the first element of s for which match returns true, and true.
// match is not called after the first match. When nothing matches, or s is nil
// or empty, Find returns the zero value of T and false.
func Find[S ~[]T, T any](s S, match func(T) bool) (T, bool) {
for _, v := range s {
if match(v) {
return v, true
}
}
var zero T
return zero, false
}

303
modules/sieve/sieve_test.go Normal file
View File

@@ -0,0 +1,303 @@
package sieve
import (
"errors"
"slices"
"strconv"
"testing"
)
type ids []int
type item struct {
ID int
Name string
}
func TestMap(t *testing.T) {
t.Run("nil input", func(t *testing.T) {
got := Map([]int(nil), func(v int) string { return strconv.Itoa(v) })
if got == nil || len(got) != 0 {
t.Fatalf("Map(nil) = %#v, want non-nil empty", got)
}
})
t.Run("order and call count", func(t *testing.T) {
in := []int{3, 1, 2}
snap := slices.Clone(in)
calls := 0
got := Map(in, func(v int) int { calls++; return v * 10 })
if !slices.Equal(got, []int{30, 10, 20}) {
t.Fatalf("Map = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d, want %d", calls, len(in))
}
got[0] = 99
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
})
}
func TestTryMap(t *testing.T) {
t.Run("success", func(t *testing.T) {
got, err := TryMap([]string{"1", "2", "3"}, strconv.Atoi)
if err != nil || !slices.Equal(got, []int{1, 2, 3}) {
t.Fatalf("TryMap = %v, %v", got, err)
}
})
t.Run("nil input", func(t *testing.T) {
got, err := TryMap([]string(nil), strconv.Atoi)
if err != nil || got == nil || len(got) != 0 {
t.Fatalf("TryMap(nil) = %#v, %v", got, err)
}
})
sentinel := errors.New("boom")
for _, failAt := range []int{0, 2} {
t.Run("fails at "+strconv.Itoa(failAt), func(t *testing.T) {
calls := 0
got, err := TryMap([]int{0, 1, 2, 3}, func(v int) (int, error) {
calls++
if v == failAt {
return 0, sentinel
}
return v, nil
})
if got != nil {
t.Fatalf("result = %#v, want nil", got)
}
if err != sentinel || !errors.Is(err, sentinel) {
t.Fatalf("err = %v, want sentinel", err)
}
if calls != failAt+1 {
t.Fatalf("calls = %d, want %d", calls, failAt+1)
}
})
}
}
func TestFilter(t *testing.T) {
even := func(v int) bool { return v%2 == 0 }
tests := []struct {
name string
in []int
keep func(int) bool
want []int
}{
{"nil input", nil, even, []int{}},
{"no matches", []int{1, 3}, even, []int{}},
{"some matches", []int{1, 2, 3, 4}, even, []int{2, 4}},
{"all match", []int{2, 4}, even, []int{2, 4}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
snap := slices.Clone(tt.in)
got := Filter(tt.in, tt.keep)
if got == nil || !slices.Equal(got, tt.want) {
t.Fatalf("Filter = %#v, want %v", got, tt.want)
}
if len(got) > 0 {
got[0] = -1
}
if !slices.Equal(tt.in, snap) {
t.Fatalf("input changed: %v", tt.in)
}
})
}
t.Run("named type", func(t *testing.T) {
var got ids = Filter(ids{1, 2, 3}, even)
if !slices.Equal(got, ids{2}) {
t.Fatalf("Filter = %v", got)
}
})
}
func TestReduce(t *testing.T) {
sum := func(a, v int) int { return a + v }
if got := Reduce([]int{1, 2, 3}, 0, sum); got != 6 {
t.Fatalf("sum = %d", got)
}
if got := Reduce([]int(nil), 42, sum); got != 42 {
t.Fatalf("Reduce(nil) = %d, want init", got)
}
if got := Reduce([]string{"a", "b", "c"}, "", func(a, v string) string { return a + v }); got != "abc" {
t.Fatalf("concat = %q", got)
}
if got := Reduce([]int{1, 2}, "", func(a string, v int) string { return a + strconv.Itoa(v) }); got != "12" {
t.Fatalf("string accumulator = %q", got)
}
counts := Reduce([]int{1, 2, 1}, map[int]int{}, func(m map[int]int, v int) map[int]int { m[v]++; return m })
if counts[1] != 2 || counts[2] != 1 {
t.Fatalf("map accumulator = %v", counts)
}
}
func TestKeyBy(t *testing.T) {
in := []item{{1, "a"}, {2, "b"}, {1, "c"}}
calls := 0
got := KeyBy(in, func(it item) int { calls++; return it.ID })
if len(got) != 2 || got[1].Name != "c" || got[2].Name != "b" {
t.Fatalf("KeyBy = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d", calls)
}
empty := KeyBy([]item(nil), func(it item) int { return it.ID })
if empty == nil || len(empty) != 0 {
t.Fatalf("KeyBy(nil) = %#v", empty)
}
}
func TestGroupBy(t *testing.T) {
in := ids{1, 2, 3, 4, 5}
snap := slices.Clone(in)
var got map[bool]ids = GroupBy(in, func(v int) bool { return v%2 == 0 })
if !slices.Equal(got[false], ids{1, 3, 5}) || !slices.Equal(got[true], ids{2, 4}) {
t.Fatalf("GroupBy = %v", got)
}
odd := got[false]
got[false] = append(got[false], 7)
got[false][0] = 100
odd[1] = 200
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
if !slices.Equal(got[true], ids{2, 4}) {
t.Fatalf("other group changed: %v", got[true])
}
empty := GroupBy([]int(nil), func(v int) int { return v })
if empty == nil || len(empty) != 0 {
t.Fatalf("GroupBy(nil) = %#v", empty)
}
}
func TestUniq(t *testing.T) {
in := []int{3, 1, 3, 2, 1}
snap := slices.Clone(in)
if got := Uniq(in); !slices.Equal(got, []int{3, 1, 2}) {
t.Fatalf("Uniq = %v", got)
}
if !slices.Equal(in, snap) {
t.Fatalf("input changed: %v", in)
}
distinct := []int{1, 2, 3}
got := Uniq(distinct)
got[0] = 9
if distinct[0] != 1 {
t.Fatalf("Uniq aliased its input")
}
empty := Uniq([]int(nil))
if empty == nil || len(empty) != 0 {
t.Fatalf("Uniq(nil) = %#v", empty)
}
var named ids = Uniq(ids{1, 1})
if !slices.Equal(named, ids{1}) {
t.Fatalf("Uniq(ids) = %v", named)
}
}
func TestUniqBy(t *testing.T) {
in := []item{{1, "a"}, {2, "b"}, {1, "c"}, {3, "d"}, {2, "e"}}
calls := 0
got := UniqBy(in, func(it item) int { calls++; return it.ID })
want := []item{{1, "a"}, {2, "b"}, {3, "d"}}
if !slices.Equal(got, want) {
t.Fatalf("UniqBy = %v", got)
}
if calls != len(in) {
t.Fatalf("calls = %d", calls)
}
empty := UniqBy([]item(nil), func(it item) int { return it.ID })
if empty == nil || len(empty) != 0 {
t.Fatalf("UniqBy(nil) = %#v", empty)
}
}
func TestPartition(t *testing.T) {
even := func(v int) bool { return v%2 == 0 }
matched, rest := Partition([]int{1, 2, 3, 4, 5}, even)
if !slices.Equal(matched, []int{2, 4}) || !slices.Equal(rest, []int{1, 3, 5}) {
t.Fatalf("Partition = %v, %v", matched, rest)
}
matched, rest = Partition([]int(nil), even)
if matched == nil || rest == nil || len(matched) != 0 || len(rest) != 0 {
t.Fatalf("Partition(nil) = %#v, %#v", matched, rest)
}
in := []int{2, 4}
matched, rest = Partition(in, even)
if rest == nil || len(rest) != 0 || !slices.Equal(matched, in) {
t.Fatalf("Partition(all) = %#v, %#v", matched, rest)
}
matched[0] = 9
if in[0] != 2 {
t.Fatalf("Partition aliased its input")
}
var m, r ids = Partition(ids{1, 2}, even)
if !slices.Equal(m, ids{2}) || !slices.Equal(r, ids{1}) {
t.Fatalf("Partition(ids) = %v, %v", m, r)
}
}
func TestFind(t *testing.T) {
calls := 0
got, ok := Find([]int{1, 4, 6, 8}, func(v int) bool { calls++; return v%2 == 0 })
if !ok || got != 4 {
t.Fatalf("Find = %d, %v", got, ok)
}
if calls != 2 {
t.Fatalf("calls = %d, want 2", calls)
}
got, ok = Find([]int{1, 3}, func(v int) bool { return v%2 == 0 })
if ok || got != 0 {
t.Fatalf("Find(no match) = %d, %v", got, ok)
}
s, ok := Find([]string(nil), func(string) bool { return true })
if ok || s != "" {
t.Fatalf("Find(nil) = %q, %v", s, ok)
}
}
func TestSet(t *testing.T) {
s := SetOf(1, 2, 2, 3)
if len(s) != 3 {
t.Fatalf("len = %d", len(s))
}
if !s.Has(2) || s.Has(4) {
t.Fatalf("Has wrong: %v", s)
}
s.Add(4, 5, 4)
if len(s) != 5 || !s.Has(5) {
t.Fatalf("Add wrong: %v", s)
}
delete(s, 1)
if s.Has(1) {
t.Fatalf("delete had no effect")
}
got := s.Slice()
slices.Sort(got)
if !slices.Equal(got, []int{2, 3, 4, 5}) {
t.Fatalf("Slice = %v", got)
}
empty := SetOf[string]()
if empty == nil {
t.Fatalf("SetOf() is nil")
}
empty.Add("a")
if !empty.Has("a") {
t.Fatalf("Add on SetOf() failed")
}
from := SetOf(ids{7, 7, 8}...)
if len(from) != 2 {
t.Fatalf("SetOf(ids...) = %v", from)
}
var zero Set[int]
if zero.Has(1) {
t.Fatalf("nil Set Has true")
}
if sl := zero.Slice(); sl == nil || len(sl) != 0 {
t.Fatalf("nil Set Slice = %#v", sl)
}
}