Files
summercms/modules/sieve/sieve.go
Jakub Zych a0bf8ece05 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.
2026-10-11 14:27:13 +02:00

153 lines
4.9 KiB
Go

// Package sieve provides generic, dependency-free collection helpers that the
// standard library slices and maps packages lack.
//
// Every function is eager and never modifies its input. Results never share 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.
// Nil or empty input yields a non-nil empty slice or map, so encoding/json
// writes [] or {} rather than null.
package sieve
// Map returns a new slice holding f applied to each element of s, in order.
// f is called once per element. A nil or empty s yields a non-nil empty slice.
//
// Map is also the pluck operation: pass an accessor func to extract one field
// from every element, for example the titles of a slice of posts.
func Map[S ~[]T, T, U any](s S, f func(T) U) []U {
out := make([]U, 0, len(s))
for _, v := range s {
out = append(out, f(v))
}
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
}