# sieve Generic, dependency-free collection helpers that the standard library slices and maps packages lack: mapping, filtering, reducing, keying, grouping, deduplicating, partitioning and finding, plus a small set type. `import "git.golem15.com/golem15/summercms/modules/sieve"` **Name:** A sand sieve: you shake a collection through it and keep what you need; map, filter and reduce for slices and maps. ## Overview `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. 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; 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 ( "fmt" "strconv" "git.golem15.com/golem15/summercms/modules/sieve" ) type Post struct { ID int Author string Title string Published bool } 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 - SummerCMS modules: none. - Third-party: none. - Standard library: none (the package has no imports). ## Testing ```sh go test ./modules/sieve/... ``` 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.